Files
ray-project--ray/rllib/env/tests/test_multi_agent_episode.py
2026-07-13 13:17:40 +08:00

4099 lines
169 KiB
Python

import unittest
from typing import Any, Callable, Dict, Optional, Tuple
import gymnasium as gym
import numpy as np
import ray
from ray.rllib.algorithms.ppo.ppo import PPOConfig
from ray.rllib.core.columns import Columns
from ray.rllib.core.rl_module.rl_module import RLModule, RLModuleSpec
from ray.rllib.env.multi_agent_env import MultiAgentEnv
from ray.rllib.env.multi_agent_env_runner import MultiAgentEnvRunner
from ray.rllib.env.multi_agent_episode import MultiAgentEpisode
from ray.rllib.utils.annotations import override
from ray.rllib.utils.test_utils import check
from ray.rllib.utils.typing import MultiAgentDict
class MultiAgentTestEnv(MultiAgentEnv):
def __init__(self, truncate=True):
super().__init__()
self.t = 0
self._agent_ids = {"agent_" + str(i) for i in range(10)}
self.observation_space = gym.spaces.Dict(
{agent_id: gym.spaces.Discrete(201) for agent_id in self._agent_ids}
)
self.action_space = gym.spaces.Dict(
{agent_id: gym.spaces.Discrete(200) for agent_id in self._agent_ids}
)
self._agents_alive = set(self._agent_ids)
self.truncate = truncate
def reset(
self,
*,
seed: Optional[int] = None,
options: Optional[dict] = None,
) -> Tuple[MultiAgentDict, MultiAgentDict]:
# Call the super's reset function.
super().reset(seed=seed, options=options)
# Set the timestep back to zero.
self.t = 0
# The number of agents that are ready at this timestep.
# Note, if we want to use an RNG, we need to use the one from the
# `gym.Env` otherwise results are not reproducible. This RNG is
# stored to `self._np_random`.
num_agents_step = self._np_random.integers(1, len(self._agent_ids) + 1)
# The agents that are ready.
agents_step = self._np_random.choice(
np.array(sorted(self._agent_ids)), num_agents_step, replace=False
)
# Initialize observations.
init_obs = {agent_id: 0 for agent_id in agents_step}
init_info = {agent_id: {} for agent_id in agents_step}
# Reset all alive agents to all agents.
self._agents_alive = set(self._agent_ids)
return init_obs, init_info
def step(
self, action_dict: MultiAgentDict
) -> Tuple[
MultiAgentDict, MultiAgentDict, MultiAgentDict, MultiAgentDict, MultiAgentDict
]:
# Increase the timestep by one.
self.t += 1
# The number of agents that are ready at this timestep.
num_agents_step = self._np_random.integers(1, len(self._agents_alive) + 1)
# The agents that are ready.
agents_step = self._np_random.choice(
np.array(sorted(self._agents_alive)), num_agents_step, replace=False
)
# If we are about to truncate, we need to make sure to provide each still-alive
# agent with an obs, otherwise, the final obs would be missing and we would
# receive an error in ma-episode.
if self.t >= 200 and self.truncate:
agents_step = self._agents_alive
# Initialize observations.
obs = {agent_id: self.t for agent_id in agents_step}
info = {agent_id: {} for agent_id in agents_step}
reward = {agent_id: 1.0 for agent_id in agents_step}
# Add also agents without observations.
reward.update(
{
agent_id: 1.0
for agent_id in self._np_random.choice(
np.array(sorted(self._agents_alive)), num_agents_step, replace=False
)
if agent_id not in reward
}
)
# Use tha last terminateds/truncateds.
is_truncated = {"__all__": False}
is_truncated.update({agent_id: False for agent_id in agents_step})
is_terminated = {"__all__": False}
is_terminated.update({agent_id: False for agent_id in agents_step})
if self.t == 50:
# Let agent 1 die.
is_terminated["agent_1"] = True
is_truncated["agent_1"] = False
# Ensure that the set of alive agents is updated.
self._agents_alive -= {"agent_1"}
# Any terminated agent, terminates with an observation.
obs.update({"agent_1": self.t})
reward.update({"agent_1": 1.0})
info.update({"agent_1": {}})
if self.t == 100 and "agent_5":
# Let agent 5 die.
is_terminated["agent_5"] = True
is_truncated["agent_5"] = False
# Ensure that the set of alive agents is updated.
self._agents_alive -= {"agent_5"}
# Any terminated agent, terminates with an observation.
obs.update({"agent_5": self.t})
reward.update({"agent_5": 1.0})
info.update({"agent_5": {}})
# Truncate the episode if too long.
if self.t >= 200 and self.truncate:
is_truncated["__all__"] = True
is_truncated.update({agent_id: True for agent_id in agents_step})
return obs, reward, is_terminated, is_truncated, info
# TODO (simon): Test `get_state()` and `from_state()`.
class TestMultiAgentEpisode(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
ray.init()
@classmethod
def tearDownClass(cls) -> None:
ray.shutdown()
def test_init(self):
# Create an empty episode.
episode = MultiAgentEpisode()
# Empty episode should have a start point and count of zero.
self.assertTrue(episode.env_t_started == episode.env_t == 0)
# Create an episode with a specific starting point, but no data.
episode = MultiAgentEpisode(env_t_started=10)
self.assertTrue(episode.env_t == episode.env_t_started == 10)
# Generate a simple multi-agent episode and check all internals after
# construction.
observations = [{"a0": 0, "a1": 0}, {"a1": 1}, {"a1": 2}, {"a1": 3}]
actions = [{"a0": 0, "a1": 0}, {"a1": 1}, {"a1": 2}]
rewards = [{"a0": 0.1, "a1": 0.1}, {"a1": 0.2}, {"a1": 0.3}]
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
agent_t_started={"a0": 0, "a1": 3},
)
check(episode.agent_episodes["a0"].observations.data, [0])
check(episode.agent_episodes["a1"].observations.data, [0, 1, 2, 3])
check(episode.agent_episodes["a0"].actions.data, [])
check(episode.agent_episodes["a1"].actions.data, [0, 1, 2])
check(episode.agent_episodes["a0"].rewards.data, [])
check(episode.agent_episodes["a1"].rewards.data, [0.1, 0.2, 0.3])
check(episode._hanging_actions_end, {"a0": 0})
check(episode._hanging_rewards_end, {"a0": 0.1})
check(episode._hanging_extra_model_outputs_end, {"a0": {}})
check(episode.env_t_to_agent_t["a0"].data, [0, "S", "S", "S"])
check(episode.env_t_to_agent_t["a1"].data, [0, 1, 2, 3])
check(episode.env_t_to_agent_t["a0"].lookback, 3)
check(episode.env_t_to_agent_t["a1"].lookback, 3)
# One of the agents doesn't step after reset.
observations = [{"a0": 0}, {"a1": 1}, {"a0": 2, "a1": 2}, {"a1": 3}, {"a1": 4}]
actions = [{"a0": 0}, {"a1": 1}, {"a0": 2, "a1": 2}, {"a1": 3}]
rewards = [{"a0": 0.1}, {"a1": 0.2}, {"a0": 0.3, "a1": 0.3}, {"a1": 0.4}]
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
agent_t_started={"a0": 1, "a1": 3},
)
check(episode.agent_episodes["a0"].observations.data, [0, 2])
check(episode.agent_episodes["a1"].observations.data, [1, 2, 3, 4])
check(episode.agent_episodes["a0"].actions.data, [0])
check(episode.agent_episodes["a1"].actions.data, [1, 2, 3])
check(episode.agent_episodes["a0"].rewards.data, [0.1])
check(episode.agent_episodes["a1"].rewards.data, [0.2, 0.3, 0.4])
check(episode._hanging_actions_end, {"a0": 2})
check(episode._hanging_rewards_end, {"a0": 0.3})
check(episode._hanging_extra_model_outputs_end, {"a0": {}})
check(episode.env_t_to_agent_t["a0"].data, [0, "S", 1, "S", "S"])
check(episode.env_t_to_agent_t["a1"].data, ["S", 0, 1, 2, 3])
check(episode.env_t_to_agent_t["a0"].lookback, 4)
check(episode.env_t_to_agent_t["a1"].lookback, 4)
# Sample 100 values and initialize the episode with observations.
env = MultiAgentTestEnv()
# Initialize containers.
observations = []
rewards = []
actions = []
infos = []
terminateds = {}
truncateds = {}
extra_model_outputs = []
agent_0_steps = []
agent_0_num_steps = 0
# Initialize observation and info.
obs, info = env.reset(seed=0)
# If "agent_0" is part of the reset obs, it steps in the first ts.
agent_0_steps.append(
agent_0_num_steps if "agent_0" in obs else episode.SKIP_ENV_TS_TAG
)
if "agent_0" in obs:
agent_0_num_steps += 1
observations.append(obs)
infos.append(info)
# Run 100 samples.
for i in range(100):
agents_to_step_next = [
aid for aid in obs.keys() if aid in env._agents_alive
]
action = {agent_id: i + 1 for agent_id in agents_to_step_next}
obs, reward, terminated, truncated, info = env.step(action)
# If "agent_0" is part of the reset obs, it steps in the first ts.
agent_0_steps.append(
agent_0_num_steps if "agent_0" in obs else episode.SKIP_ENV_TS_TAG
)
if "agent_0" in obs:
agent_0_num_steps += 1
observations.append(obs)
actions.append(action)
rewards.append(reward)
infos.append(info)
terminateds.update(terminated)
truncateds.update(truncated)
extra_model_outputs.append(
{agent_id: {"extra_1": 10.5} for agent_id in agents_to_step_next}
)
# Now create the episode from the recorded data. Pretend that the given data
# is all part of the lookback buffer and the episode (chunk) started at the
# end of that lookback buffer data.
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
infos=infos,
terminateds=terminateds,
truncateds=truncateds,
extra_model_outputs=extra_model_outputs,
env_t_started=len(rewards),
agent_t_started={"agent_0": agent_0_num_steps - 1},
len_lookback_buffer="auto", # default
)
# The starting point and count should now be at `len(observations) - 1`.+
self.assertTrue(episode.env_t == episode.env_t_started == len(rewards))
# Assert that agent 1 and agent 5 are both terminated.
self.assertTrue(episode.agent_episodes["agent_1"].is_terminated)
self.assertTrue(episode.agent_episodes["agent_5"].is_terminated)
# Assert that the other agents are neither terminated nor truncated.
for agent_id in env.agents:
if agent_id != "agent_1" and agent_id != "agent_5":
self.assertFalse(episode.agent_episodes[agent_id].is_done)
# Assert that the agent_0 env_t_to_agent_t mapping is correct:
check(episode.env_t_to_agent_t["agent_0"].data, agent_0_steps)
# Test now initializing an episode and setting its starting timestep.
episode = MultiAgentEpisode(
observations=observations[-11:],
actions=actions[-10:],
rewards=rewards[-10:],
infos=infos[-11:],
terminateds=terminateds,
truncateds=truncateds,
extra_model_outputs=extra_model_outputs[-10:],
env_t_started=100,
agent_t_started={"agent_5": 8},
len_lookback_buffer="auto", # default: all data goes into lookback buffers
)
# Assert that the episode starts indeed at 100.
check(episode.env_t, episode.env_t_started, 100)
# B/c all data went into lookback buffers, all single-agent episodes and
# the multi-agent episode itself should have len=0.
check(len(episode), 0)
for agent_id in episode.agent_ids:
check(len(episode.agent_episodes[agent_id]), 0)
check(len(episode.agent_episodes[agent_id].observations), 1)
check(len(episode.agent_episodes[agent_id].actions), 0)
check(len(episode.agent_episodes[agent_id].rewards), 0)
check(episode.agent_episodes[agent_id].is_truncated, False)
check(episode.agent_episodes[agent_id].is_numpy, False)
check(episode.agent_episodes["agent_5"].is_terminated, True)
check(
episode.env_t_to_agent_t["agent_5"].data,
["S", 0, 1, "S", 2, 3, 4, 5, 6, 7, 8],
)
# Now test, if agents that have never stepped are handled correctly.
# agent 5 will be the agent that never stepped.
(
observations,
actions,
rewards,
terminateds,
truncateds,
infos,
) = self._mock_multi_agent_records()
# Create the episode from the mock data.
episode = MultiAgentEpisode(
# agent_ids=["agent_1", "agent_2", "agent_3", "agent_4", "agent_5"],
observations=observations,
actions=actions,
rewards=rewards,
infos=infos,
terminateds=terminateds,
truncateds=truncateds,
len_lookback_buffer=0,
)
# Assert that the length of `SingleAgentEpisode`s are all correct.
check(len(episode.agent_episodes["agent_1"]), 1)
check(len(episode.agent_episodes["agent_2"]), 1)
check(len(episode.agent_episodes["agent_3"]), 1)
check(len(episode.agent_episodes["agent_4"]), 1)
# check(len(episode.agent_episodes["agent_5"]), 0)
# TODO (simon): Also test the other structs inside the MAE for agent 5 and
# the other agents.
def test_add_env_reset(self):
# Generate an environment.
env = MultiAgentTestEnv()
# Generate an empty multi-agent episode. Note. we have to provide the
# agent ids.
episode = MultiAgentEpisode(
observation_space=env.observation_space,
action_space=env.action_space,
)
# Generate initial observations and infos and add them to the episode.
obs, infos = env.reset(seed=0)
episode.add_env_reset(
observations=obs,
infos=infos,
)
# Assert that timestep is at zero.
self.assertTrue(episode.env_t == episode.env_t_started == 0)
# Assert that the agents with initial observations have their single-agent
# episodes in place.
for agent_id in env.agents:
# Ensure that all agents have a single env_ts=0 -> agent_ts=0
# entry in their env- to agent-timestep mappings.
if agent_id in obs:
self.assertGreater(
len(episode.agent_episodes[agent_id].observations), 0
)
self.assertGreater(len(episode.agent_episodes[agent_id].infos), 0)
check(episode.env_t_to_agent_t[agent_id].data, [0])
# Agents that have no reset obs, will not step in next ts -> They should NOT
# have a single agent episod yet and their mappings should be empty.
else:
self.assertTrue(agent_id not in episode.agent_episodes)
check(episode.env_t_to_agent_t[agent_id].data, [])
# TODO (simon): Test the buffers and reward storage.
def test_add_env_step(self):
# Create an environment and add the initial observations and infos.
env = MultiAgentTestEnv()
episode = MultiAgentEpisode()
agent_0_steps = []
agent_0_num_steps = 0
obs, infos = env.reset(seed=10)
episode.add_env_reset(
observations=obs,
infos=infos,
)
# If "agent_0" is part of the reset obs, it steps in the first ts.
agent_0_steps.append(
agent_0_num_steps if "agent_0" in obs else episode.SKIP_ENV_TS_TAG
)
if "agent_0" in obs:
agent_0_num_steps += 1
# Sample 100 timesteps and add them to the episode.
for i in range(100):
action = {
agent_id: i + 1 for agent_id in obs if agent_id in env._agents_alive
}
obs, reward, terminated, truncated, info = env.step(action)
# If "agent_0" is part of the reset obs, it steps in the first ts.
agent_0_steps.append(
agent_0_num_steps if "agent_0" in obs else episode.SKIP_ENV_TS_TAG
)
if "agent_0" in obs:
agent_0_num_steps += 1
episode.add_env_step(
observations=obs,
actions=action,
rewards=reward,
infos=info,
terminateds=terminated,
truncateds=truncated,
extra_model_outputs={agent_id: {"extra": 10.5} for agent_id in action},
)
# Assert that the timestep is at 100.
check(episode.env_t, 100)
# Ensure that the episode is not done yet.
self.assertFalse(episode.is_done)
# Ensure that agent 1 and agent 5 are indeed done.
self.assertTrue(episode.agent_episodes["agent_1"].is_done)
self.assertTrue(episode.agent_episodes["agent_5"].is_done)
# Also ensure that their buffers are all empty:
for agent_id in ["agent_1", "agent_5"]:
self.assertTrue(agent_id not in episode._hanging_actions_end)
self.assertTrue(agent_id not in episode._hanging_rewards_end)
self.assertTrue(agent_id not in episode._hanging_extra_model_outputs_end)
# Check validity of agent_0's env_t_to_agent_t mapping.
check(episode.env_t_to_agent_t["agent_0"].data, agent_0_steps)
# Run another 100 timesteps.
for i in range(100, 200):
action = {
agent_id: i + 1 for agent_id in obs if agent_id in env._agents_alive
}
obs, reward, terminated, truncated, info = env.step(action)
episode.add_env_step(
observations=obs,
actions=action,
rewards=reward,
infos=info,
terminateds=terminated,
truncateds=truncated,
extra_model_outputs={agent_id: {"extra": 10.5} for agent_id in action},
)
# Assert that the environment is done.
self.assertTrue(truncated["__all__"])
# Assert that each agent is done.
for agent_id in episode.agent_ids:
self.assertTrue(episode.agent_episodes[agent_id].is_done)
# Assert that agent 1 and agent 5 have no observations/actions/etc.
# after the timesteps in which they terminated.
self.assertGreaterEqual(50, episode.agent_episodes["agent_1"].observations[-1])
self.assertGreaterEqual(50, episode.agent_episodes["agent_1"].actions[-1])
self.assertGreaterEqual(100, episode.agent_episodes["agent_5"].observations[-1])
self.assertGreaterEqual(100, episode.agent_episodes["agent_5"].actions[-1])
# Now test, if agents that have never stepped are handled correctly.
# agent 5 will be the agent that never stepped.
(
observations,
actions,
rewards,
terminated,
truncated,
infos,
) = self._mock_multi_agent_records()
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
infos=infos,
terminateds=terminated,
truncateds=truncated,
# len_lookback_buffer=0,
# agent_t_started={},
)
# Now test that intermediate rewards will get recorded and actions buffered.
action = {"agent_2": 3, "agent_4": 3}
reward = {"agent_1": 1.0, "agent_2": 1.0, "agent_3": 1.0, "agent_5": 1.0}
observation = {"agent_1": 3, "agent_2": 3}
infos = {"agent_1": {}, "agent_2": {}}
terminated = {k: False for k in observation.keys()}
terminated.update({"__all__": False})
truncated = {k: False for k in observation.keys()}
truncated.update({"__all__": False})
episode.add_env_step(
observations=observation,
actions=action,
rewards=reward,
infos=infos,
terminateds=terminated,
truncateds=truncated,
)
# Assert that the action cache for agent 4 is used.
# Note, agent 4 acts, but receives no observation.
# Note also, all other caches are always used, due to their defaults.
self.assertTrue(episode._hanging_actions_end["agent_4"] is not None)
# Assert that the reward caches of agents 3 and 5 are there.
# For agent_5 (b/c it has never done anything), we add to the begin cache.
check(episode._hanging_rewards_end["agent_3"], 2.2)
check(episode._hanging_rewards_begin["agent_5"], 1.0)
def test_get_observations(self):
# Generate simple records for a multi agent environment.
(
observations,
actions,
rewards,
is_terminateds,
is_truncateds,
infos,
) = self._mock_multi_agent_records()
# Create a multi-agent episode.
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
infos=infos,
terminateds=is_terminateds,
truncateds=is_truncateds,
len_lookback_buffer="auto", # default: use all data as lookback
)
# Get last observations for the multi-agent episode.
obs = episode.get_observations(indices=-1)
check(obs, {"agent_2": 2, "agent_4": 2})
# Return last two observations for the entire env.
# Also, we flip the indices here and require -1 before -2, this
# should reflect in the returned results.
obs = episode.get_observations(indices=[-1, -2])
# Note, agent 4 has two observations in the last two ones.
# Note, `get_observations()` returns in the order of the `indices` arg.
check(obs, {"agent_1": [1], "agent_2": [2], "agent_3": [1], "agent_4": [2, 1]})
# Return last two observations for the entire env using slice.
obs = episode.get_observations(slice(-2, None))
check(
obs,
{"agent_1": [1], "agent_2": [2], "agent_3": [1], "agent_4": [1, 2]},
)
# Return last four observations for the entire env using slice
# and `fill`.
obs = episode.get_observations(slice(-5, None), fill=-10)
check(
obs,
{
# All first two ts should be 0s (fill before episode even
# started).
# 3rd items are the reset obs for agents
"agent_1": [-10, -10, 0, 1, -10], # ag1 stepped the first 2 ts
"agent_2": [-10, -10, 0, -10, 2], # ag2 stepped first and last ts
"agent_3": [-10, -10, 0, 1, -10], # ag3 same as ag1
"agent_4": [-10, -10, -10, 1, 2], # ag4 steps in last 2 ts
},
)
# Use `fill` to look into the future (ts=100 and 101).
obs = episode.get_observations(slice(100, 102), fill=9.9)
check(
obs,
{
"agent_1": [9.9, 9.9],
"agent_2": [9.9, 9.9],
"agent_3": [9.9, 9.9],
"agent_4": [9.9, 9.9],
},
)
# Return two observations in lookback buffers for the entire env using
# `neg_index_as_lookback=True` and an index list.
# w/ fill
obs = episode.get_observations(
indices=[-2, -1],
fill=-10,
neg_index_as_lookback=True,
)
check(
obs,
{
"agent_1": [0, 1],
"agent_2": [0, -10],
"agent_3": [0, 1],
"agent_4": [-10, 1],
},
)
# Same, but w/o fill
obs = episode.get_observations(indices=[-2, -1], neg_index_as_lookback=True)
check(
obs,
{"agent_1": [0, 1], "agent_2": [0], "agent_3": [0, 1], "agent_4": [1]},
)
# Get last observations for each individual agent.
obs = episode.get_observations(indices=-1, env_steps=False)
check(obs, {"agent_1": 1, "agent_2": 2, "agent_3": 1, "agent_4": 2})
# Same, but with `agent_ids` filters.
obs = episode.get_observations(-1, env_steps=False, agent_ids="agent_1")
check(obs, {"agent_1": 1})
obs = episode.get_observations(-1, env_steps=False, agent_ids=["agent_2"])
check(obs, {"agent_2": 2})
obs = episode.get_observations(-1, env_steps=False, agent_ids=("agent_3",))
check(obs, {"agent_3": 1})
obs = episode.get_observations(-1, env_steps=False, agent_ids={"agent_4"})
check(obs, {"agent_4": 2})
obs = episode.get_observations(
-1, env_steps=False, agent_ids=["agent_1", "agent_2"]
)
check(obs, {"agent_1": 1, "agent_2": 2})
obs = episode.get_observations(-2, env_steps=True, agent_ids={"agent_4"})
check(obs, {"agent_4": 1})
obs = episode.get_observations([-1, -2], env_steps=True, agent_ids={"agent_4"})
check(obs, {"agent_4": [2, 1]})
# Return the last two observations for each individual agent.
obs = episode.get_observations(indices=[-1, -2], env_steps=False)
check(
obs,
{
"agent_1": [1, 0],
"agent_2": [2, 0],
"agent_3": [1, 0],
"agent_4": [2, 1],
},
)
# Now, test the same when returning a list.
obs = episode.get_observations(return_list=True)
check(obs, [{"agent_2": 2, "agent_4": 2}])
# Expect error when calling with env_steps=False.
with self.assertRaises(ValueError):
episode.get_observations(env_steps=False, return_list=True)
# List of indices.
obs = episode.get_observations(indices=[-1, -2], return_list=True)
check(
obs,
[
{"agent_2": 2, "agent_4": 2},
{"agent_1": 1, "agent_3": 1, "agent_4": 1},
],
)
# Slice of indices w/ fill.
obs = episode.get_observations(
slice(-1, 1),
return_list=True,
fill=-8,
neg_index_as_lookback=True,
)
check(
obs,
[
{"agent_1": 1, "agent_2": -8, "agent_3": 1, "agent_4": 1},
{"agent_1": -8, "agent_2": 2, "agent_3": -8, "agent_4": 2},
],
)
# B/c we have lookback="auto" in the ma episode, all data we sent into
# the c"tor was pushed into the lookback buffers and only the last
# observations are outside these buffers and will be returned here.
obs = episode.get_observations(env_steps=False)
check(
obs,
{"agent_1": [1], "agent_2": [2], "agent_3": [1], "agent_4": [2]},
)
# Test with initial observations only.
episode = MultiAgentEpisode()
episode.add_env_reset(
observations=observations[0],
infos=infos[0],
)
# Get the last observation for agents and assert that they are correct.
obs = episode.get_observations()
for agent_id, agent_obs in observations[0].items():
check(obs[agent_id][0], agent_obs)
# Now the same as list.
obs = episode.get_observations(return_list=True)
for agent_id, agent_obs in observations[0].items():
check(obs[0][agent_id], agent_obs)
# Now by agent steps.
obs = episode.get_observations(env_steps=False)
for agent_id, agent_obs in observations[0].items():
check(obs[agent_id][0], agent_obs)
def test_get_infos(self):
# Generate simple records for a multi agent environment.
(
observations,
actions,
rewards,
is_terminateds,
is_truncateds,
infos,
) = self._mock_multi_agent_records()
# Create a multi-agent episode.
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
infos=infos,
terminateds=is_terminateds,
truncateds=is_truncateds,
len_lookback_buffer="auto", # default: use all data as lookback
)
# Get last infos for the multi-agent episode.
inf = episode.get_infos(indices=-1)
check(inf, infos[-1])
# Return last two infos for the entire env.
# Also, we flip the indices here and require -1 before -2, this
# should reflect in the returned results.
inf = episode.get_infos(indices=[-1, -2])
# Note, agent 4 has two infos in the last two ones.
# Note, `get_infos()` returns in the order of the `indices` arg.
check(
inf,
{
"agent_1": [{"a1_i1": 1.1}],
"agent_2": [{"a2_i2": 2.2}],
"agent_3": [{"a3_i1": 3.1}],
"agent_4": [{"a4_i2": 4.2}, {"a4_i1": 4.1}],
},
)
# Return last two infos for the entire env using slice.
inf = episode.get_infos(slice(-2, None))
check(
inf,
{
"agent_1": [{"a1_i1": 1.1}],
"agent_2": [{"a2_i2": 2.2}],
"agent_3": [{"a3_i1": 3.1}],
"agent_4": [{"a4_i1": 4.1}, {"a4_i2": 4.2}],
},
)
# Return last four infos for the entire env using slice
# and `fill`.
inf = episode.get_infos(slice(-5, None), fill={"4": "2"})
check(
inf,
{
# All first two ts should be 0s (fill before episode even
# started).
# 3rd items are the reset obs for agents
"agent_1": [
{"4": "2"},
{"4": "2"},
{"a1_i0": 1},
{"a1_i1": 1.1},
{"4": "2"},
], # ag1 stepped the first 2 ts
"agent_2": [
{"4": "2"},
{"4": "2"},
{"a2_i0": 2},
{"4": "2"},
{"a2_i2": 2.2},
], # ag2 stepped first and last ts
"agent_3": [
{"4": "2"},
{"4": "2"},
{"a3_i0": 3},
{"a3_i1": 3.1},
{"4": "2"},
], # ag3 same as ag1
"agent_4": [
{"4": "2"},
{"4": "2"},
{"4": "2"},
{"a4_i1": 4.1},
{"a4_i2": 4.2},
], # ag4 steps in last 2 ts
},
)
# Use `fill` (but as a non-dict, just to check) to look into the future
# (ts=100 and 101).
inf = episode.get_infos(slice(100, 102), fill=9.9)
check(
inf,
{
"agent_1": [9.9, 9.9],
"agent_2": [9.9, 9.9],
"agent_3": [9.9, 9.9],
"agent_4": [9.9, 9.9],
},
)
# Return two infos in lookback buffers for the entire env using
# `neg_index_as_lookback=True` and an index list.
# w/ fill
inf = episode.get_infos(
indices=[-2, -1],
fill=-10,
neg_index_as_lookback=True,
)
check(
inf,
{
"agent_1": [{"a1_i0": 1}, {"a1_i1": 1.1}],
"agent_2": [{"a2_i0": 2}, -10],
"agent_3": [{"a3_i0": 3}, {"a3_i1": 3.1}],
"agent_4": [-10, {"a4_i1": 4.1}],
},
)
# Same, but w/o fill
inf = episode.get_infos(indices=[-2, -1], neg_index_as_lookback=True)
check(
inf,
{
"agent_1": [{"a1_i0": 1}, {"a1_i1": 1.1}],
"agent_2": [{"a2_i0": 2}],
"agent_3": [{"a3_i0": 3}, {"a3_i1": 3.1}],
"agent_4": [{"a4_i1": 4.1}],
},
)
# Get last infos for each individual agent.
inf = episode.get_infos(indices=-1, env_steps=False)
check(
inf,
{
"agent_1": {"a1_i1": 1.1},
"agent_2": {"a2_i2": 2.2},
"agent_3": {"a3_i1": 3.1},
"agent_4": {"a4_i2": 4.2},
},
)
# Same, but with `agent_ids` filters.
inf = episode.get_infos(-1, env_steps=False, agent_ids="agent_1")
check(inf, {"agent_1": {"a1_i1": 1.1}})
inf = episode.get_infos(-1, env_steps=False, agent_ids=["agent_2"])
check(inf, {"agent_2": {"a2_i2": 2.2}})
inf = episode.get_infos(-1, env_steps=False, agent_ids=("agent_3",))
check(inf, {"agent_3": {"a3_i1": 3.1}})
inf = episode.get_infos(-1, env_steps=False, agent_ids={"agent_4"})
check(inf, {"agent_4": {"a4_i2": 4.2}})
inf = episode.get_infos(-1, env_steps=False, agent_ids=["agent_1", "agent_2"])
check(inf, {"agent_1": {"a1_i1": 1.1}, "agent_2": {"a2_i2": 2.2}})
inf = episode.get_infos(-2, env_steps=True, agent_ids={"agent_4"})
check(inf, {"agent_4": {"a4_i1": 4.1}})
inf = episode.get_infos([-1, -2], env_steps=True, agent_ids={"agent_4"})
check(inf, {"agent_4": [{"a4_i2": 4.2}, {"a4_i1": 4.1}]})
# Return the last two infos for each individual agent.
inf = episode.get_infos(indices=[-1, -2], env_steps=False)
check(
inf,
{
"agent_1": [{"a1_i1": 1.1}, {"a1_i0": 1}],
"agent_2": [{"a2_i2": 2.2}, {"a2_i0": 2}],
"agent_3": [{"a3_i1": 3.1}, {"a3_i0": 3}],
"agent_4": [{"a4_i2": 4.2}, {"a4_i1": 4.1}],
},
)
# Now, test the same when returning a list.
inf = episode.get_infos(return_list=True)
check(inf, [{"agent_2": {"a2_i2": 2.2}, "agent_4": {"a4_i2": 4.2}}])
# Expect error when calling with env_steps=False.
with self.assertRaises(ValueError):
episode.get_infos(env_steps=False, return_list=True)
# List of indices.
inf = episode.get_infos(indices=[-1, -2], return_list=True)
check(
inf,
[
{"agent_2": {"a2_i2": 2.2}, "agent_4": {"a4_i2": 4.2}},
{
"agent_1": {"a1_i1": 1.1},
"agent_3": {"a3_i1": 3.1},
"agent_4": {"a4_i1": 4.1},
},
],
)
# Slice of indices w/ fill.
inf = episode.get_infos(
slice(-1, 1),
return_list=True,
fill=-8,
neg_index_as_lookback=True,
)
check(
inf,
[
{
"agent_1": {"a1_i1": 1.1},
"agent_2": -8,
"agent_3": {"a3_i1": 3.1},
"agent_4": {"a4_i1": 4.1},
},
{
"agent_1": -8,
"agent_2": {"a2_i2": 2.2},
"agent_3": -8,
"agent_4": {"a4_i2": 4.2},
},
],
)
# B/c we have lookback="auto" in the ma episode, all data we sent into
# the c"tor was pushed into the lookback buffers and only the last
# infos are outside these buffers and will be returned here.
inf = episode.get_infos(env_steps=False)
check(
inf,
{
"agent_1": [{"a1_i1": 1.1}],
"agent_2": [{"a2_i2": 2.2}],
"agent_3": [{"a3_i1": 3.1}],
"agent_4": [{"a4_i2": 4.2}],
},
)
# Test with initial infos only.
episode = MultiAgentEpisode()
episode.add_env_reset(
observations=observations[0],
infos=infos[0],
)
# Get the last infos for agents and assert that they are correct.
inf = episode.get_infos()
for agent_id, agent_inf in infos[0].items():
check(inf[agent_id][0], agent_inf)
# Now the same as list.
inf = episode.get_infos(return_list=True)
for agent_id, agent_inf in infos[0].items():
check(inf[0][agent_id], agent_inf)
# Now by agent steps.
inf = episode.get_infos(env_steps=False)
for agent_id, agent_inf in infos[0].items():
check(inf[agent_id][0], agent_inf)
def test_get_actions(self):
"""Tests whether the `MultiAgentEpisode.get_actions()` API works as expected."""
# Generate a simple multi-agent episode.
observations = [
{"a0": 0, "a1": 0},
{"a0": 1, "a1": 1},
{"a1": 2},
{"a1": 3},
{"a1": 4},
]
actions = [{"a0": 0, "a1": 0}, {"a0": 1, "a1": 1}, {"a1": 2}, {"a1": 3}]
rewards = [{"a0": 1, "a1": 1}, {"a0": 2, "a1": 2}, {"a1": 3}, {"a1": 4}]
episode = MultiAgentEpisode(
observations=observations, actions=actions, rewards=rewards
)
# Access single indices, env steps.
for i in range(-1, -5, -1):
act = episode.get_actions(i)
check(act, actions[i])
# Access >=0 integer indices (expect index error as everything is in
# lookback buffer).
for i in range(0, 5):
with self.assertRaises(IndexError):
episode.get_actions(i)
# Access <= -5 integer indices (expect index error as this goes beyond length of
# lookback buffer).
for i in range(-5, -10, -1):
with self.assertRaises(IndexError):
episode.get_actions(i)
# Access list of indices, env steps.
act = episode.get_actions([-1, -2])
check(act, {"a1": [3, 2]})
act = episode.get_actions([-2, -3])
check(act, {"a0": [1], "a1": [2, 1]})
act = episode.get_actions([-3, -4])
check(act, {"a0": [1, 0], "a1": [1, 0]})
# Access slices of indices, env steps.
act = episode.get_actions(slice(-1, -3, -1))
check(act, {"a1": [3, 2]})
act = episode.get_actions(slice(-2, -4, -1))
check(act, {"a0": [1], "a1": [2, 1]})
act = episode.get_actions(slice(-3, -5, -1))
check(act, {"a0": [1, 0], "a1": [1, 0]})
act = episode.get_actions(slice(-3, -6, -1), fill="skip")
check(act, {"a0": [1, 0, "skip"], "a1": [1, 0, "skip"]})
act = episode.get_actions(slice(1, -6, -1), fill="s")
check(
act,
{"a0": ["s", "s", "s", "s", 1, 0, "s"], "a1": ["s", "s", 3, 2, 1, 0, "s"]},
)
act = episode.get_actions(slice(0, -5, -1), fill="s")
check(
act,
{"a0": ["s", "s", "s", 1, 0], "a1": ["s", 3, 2, 1, 0]},
)
# Access single indices, agent steps.
act = episode.get_actions(-1, env_steps=False)
check(act, {"a0": 1, "a1": 3})
act = episode.get_actions(-2, env_steps=False)
check(act, {"a0": 0, "a1": 2})
act = episode.get_actions(-3, env_steps=False, agent_ids="a1")
check(act, {"a1": 1})
act = episode.get_actions(-3, env_steps=False, fill="skip")
check(act, {"a0": "skip", "a1": 1})
act = episode.get_actions(-4, env_steps=False, agent_ids="a1")
check(act, {"a1": 0})
act = episode.get_actions(-4, env_steps=False, fill="skip")
check(act, {"a0": "skip", "a1": 0})
episode.add_env_step(
observations={"a0": 5, "a1": 5}, actions={"a1": 4}, rewards={"a1": 4}
)
check(episode.get_actions(0), {"a1": 4})
check(episode.get_actions(-1), {"a1": 4})
check(episode.get_actions(-2), {"a1": 3})
episode.add_env_step(
observations={"a1": 6},
actions={"a0": 5, "a1": 5},
rewards={"a0": 5, "a1": 5},
)
check(episode.get_actions(0), {"a1": 4})
check(episode.get_actions(1), {"a0": 5, "a1": 5})
check(episode.get_actions(-1), {"a0": 5, "a1": 5})
# Generate a simple multi-agent episode, where a hanging action is at the end.
observations = [
{"a0": 0, "a1": 0},
{"a0": 0, "a1": 1},
{"a0": 2},
]
actions = [{"a0": 0, "a1": 0}, {"a0": 1, "a1": 1}]
rewards = [{"a0": 0.0, "a1": 0.0}, {"a0": 0.1, "a1": 0.1}]
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
len_lookback_buffer=0,
)
# Test, whether the hanging action of a1 at the end gets returned properly
# for idx=-1.
act = episode.get_actions(-1)
check(act, {"a0": 1, "a1": 1})
act = episode.get_actions(-2)
check(act, {"a0": 0, "a1": 0})
act = episode.get_actions(0)
check(act, {"a0": 0, "a1": 0})
act = episode.get_actions(1)
check(act, {"a0": 1, "a1": 1})
with self.assertRaises(IndexError):
episode.get_actions(2)
with self.assertRaises(IndexError):
episode.get_actions(-3)
# Generate a simple multi-agent episode, where one agent is done.
# observations = [
# {"a0": 0, "a1": 0},
# {"a0": 1, "a1": 1},
# {"a0": 2},
# ]
# actions = [{"a0": 0, "a1": 0}, {"a0": 1}]
# rewards = [{"a0": 1, "a1": 1}, {"a0": 2}]
# terminateds = {"a1": True}
# episode = MultiAgentEpisode(
# observations=observations,
# actions=actions,
# rewards=rewards,
# terminateds=terminateds,
# len_lookback_buffer=0,
# )
episode = MultiAgentEpisode()
episode.add_env_reset(observations={"a0": 0, "a1": 0})
episode.add_env_step(
observations={"a0": 1, "a1": 1},
actions={"a0": 0, "a1": 0},
rewards={"a0": 0.0, "a1": 0.0},
terminateds={"a1": True},
)
episode.add_env_step(
observations={"a0": 2}, actions={"a0": 1}, rewards={"a0": 1.0}
)
act = episode.get_actions(-1)
check(act, {"a0": 1})
# Generate simple records for a multi agent environment.
(
observations,
actions,
rewards,
is_terminateds,
is_truncateds,
infos,
) = self._mock_multi_agent_records()
# Create a multi-agent episode.
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
infos=infos,
terminateds=is_terminateds,
truncateds=is_truncateds,
len_lookback_buffer="auto", # default: use all data as lookback
)
# Get last actions for the multi-agent episode.
act = episode.get_actions(indices=-1)
check(act, {"agent_1": 1, "agent_3": 1, "agent_4": 1})
# Return last two actions for the entire env.
# Also, we flip the indices here and require -1 before -2, this
# should reflect in the returned results.
act = episode.get_actions(indices=[-1, -2])
check(
act,
{"agent_1": [1, 0], "agent_2": [0], "agent_3": [1, 0], "agent_4": [1]},
)
# Return last two actions for the entire env using slice.
act = episode.get_actions(slice(-2, None))
check(
act,
{"agent_1": [0, 1], "agent_2": [0], "agent_3": [0, 1], "agent_4": [1]},
)
# Return last four actions for the entire env using slice
# and `fill`.
act = episode.get_actions(slice(-5, None), fill=-10)
check(
act,
{
# All first three ts should be 0s (fill before episode even
# started).
# 4th items are the 1st actions (after reset obs) for agents
"agent_1": [-10, -10, -10, 0, 1], # ag1 stepped the first 2 ts
"agent_2": [-10, -10, -10, 0, -10], # ag2 stepped first and last ts
"agent_3": [-10, -10, -10, 0, 1], # ag3 same as ag1
"agent_4": [-10, -10, -10, -10, 1], # ag4 steps in last 2 ts
},
)
# Use `fill` to look into the future (ts=100 and 101).
act = episode.get_actions(slice(100, 102), fill=9.9)
check(
act,
{
"agent_1": [9.9, 9.9],
"agent_2": [9.9, 9.9],
"agent_3": [9.9, 9.9],
"agent_4": [9.9, 9.9],
},
)
# Return two actions in lookback buffers for the entire env using
# `neg_index_as_lookback=True` and an index list.
# w/ fill
act = episode.get_actions(
indices=[-2, -1],
fill=-10,
neg_index_as_lookback=True,
)
check(
act,
{
"agent_1": [0, 1],
"agent_2": [0, -10],
"agent_3": [0, 1],
"agent_4": [-10, 1],
},
)
# Same, but w/o fill.
act = episode.get_actions(indices=[-2, -1], neg_index_as_lookback=True)
check(
act,
{
"agent_1": [0, 1],
"agent_2": [0],
"agent_3": [0, 1],
"agent_4": [1],
},
)
# Get last actions for each individual agent.
act = episode.get_actions(indices=-1, env_steps=False)
check(act, {"agent_1": 1, "agent_2": 0, "agent_3": 1, "agent_4": 1})
# Same, but with `agent_ids` filters.
act = episode.get_actions(-1, env_steps=False, agent_ids="agent_1")
check(act, {"agent_1": 1})
act = episode.get_actions(-1, env_steps=False, agent_ids=["agent_2"])
check(act, {"agent_2": 0})
act = episode.get_actions(-1, env_steps=False, agent_ids=("agent_3",))
check(act, {"agent_3": 1})
act = episode.get_actions(-1, env_steps=False, agent_ids={"agent_4"})
check(act, {"agent_4": 1})
act = episode.get_actions(-1, env_steps=False, agent_ids=["agent_1", "agent_2"])
check(act, {"agent_1": 1, "agent_2": 0})
act = episode.get_actions(-2, env_steps=True, agent_ids={"agent_4"})
check(act, {})
act = episode.get_actions([-1, -2], env_steps=True, agent_ids={"agent_4"})
check(act, {"agent_4": [1]})
# Agent 4 has only acted 2x, so there is no (local) ts=-2 for it.
with self.assertRaises(IndexError):
episode.get_actions([-1, -2], env_steps=False, agent_ids={"agent_4"})
act = episode.get_actions([-2], env_steps=False, agent_ids="agent_4", fill=-10)
check(act, {"agent_4": [-10]})
# Now, test the same when returning a list.
# B/c we have lookback="auto" in the ma episode, all data we sent into
# the c"tor was pushed into the lookback buffers and thus all
# actions are in these buffers (and won't get returned here).
act = episode.get_actions(return_list=True)
self.assertTrue(act == [])
# Expect error when calling with env_steps=False AND return_list=True.
with self.assertRaises(ValueError):
episode.get_actions(env_steps=False, return_list=True)
# List of indices.
act = episode.get_actions(indices=[-1, -2], return_list=True)
check(act, [actions[-1], actions[-2]])
# Slice of indices w/ fill.
# From the last ts in lookback buffer to first actual ts (empty as all data is
# in lookback buffer, but we fill).
act = episode.get_actions(
slice(-1, 1), return_list=True, fill=-8, neg_index_as_lookback=True
)
check(
act,
[
{"agent_1": 1, "agent_2": -8, "agent_3": 1, "agent_4": 1},
{"agent_1": -8, "agent_2": -8, "agent_3": -8, "agent_4": -8},
],
)
# B/c we have lookback="auto" in the ma episode, all data we sent into
# the c"tor was pushed into the lookback buffers and thus all
# actions are in these buffers.
act = episode.get_actions(env_steps=False)
self.assertTrue(act == {})
# Test with initial actions only.
episode = MultiAgentEpisode()
episode.add_env_reset(observations=observations[0], infos=infos[0])
# Get the last action for agents and assert that it's correct.
act = episode.get_actions()
check(act, {})
# Now the same as list.
act = episode.get_actions(return_list=True)
self.assertTrue(act == [])
# Now agent steps.
act = episode.get_actions(env_steps=False)
self.assertTrue(act == {})
def test_get_rewards(self):
# Generate a simple multi-agent episode.
observations = [
{"a0": 0, "a1": 0},
{"a0": 1, "a1": 1},
{"a1": 2},
{"a1": 3},
{"a1": 4},
]
actions = [{"a0": 0, "a1": 0}, {"a0": 1, "a1": 1}, {"a1": 2}, {"a1": 3}]
rewards = [
{"a0": 0.0, "a1": 0.0},
{"a0": 1.0, "a1": 1.0},
{"a1": 2.0},
{"a1": 3.0},
]
episode = MultiAgentEpisode(
observations=observations, actions=actions, rewards=rewards
)
# Access single indices, env steps.
for i in range(-1, -5, -1):
rew = episode.get_rewards(i)
check(rew, rewards[i])
# Access list of indices, env steps.
rew = episode.get_rewards([-1, -2])
check(rew, {"a0": [], "a1": [3, 2]})
rew = episode.get_rewards([-2, -3])
check(rew, {"a0": [1], "a1": [2, 1]})
rew = episode.get_rewards([-3, -4])
check(rew, {"a0": [1, 0], "a1": [1, 0]})
# Access slices of indices, env steps.
rew = episode.get_rewards(slice(-1, -3, -1))
check(rew, {"a0": [], "a1": [3, 2]})
rew = episode.get_rewards(slice(-2, -4, -1))
check(rew, {"a0": [1], "a1": [2, 1]})
rew = episode.get_rewards(slice(-3, -5, -1))
check(rew, {"a0": [1, 0], "a1": [1, 0]})
rew = episode.get_rewards(slice(-3, -6, -1), fill=-10.0)
check(rew, {"a0": [1, 0, -10.0], "a1": [1, 0, -10.0]})
rew = episode.get_rewards(slice(1, -6, -1), fill=-1)
check(
rew,
{"a0": [-1, -1, -1, -1, 1, 0, -1], "a1": [-1, -1, 3, 2, 1, 0, -1]},
)
rew = episode.get_rewards(slice(0, -5, -1), fill=-2)
check(
rew,
{"a0": [-2, -2, -2, 1, 0], "a1": [-2, 3, 2, 1, 0]},
)
# Access single indices, agent steps.
rew = episode.get_rewards(-1, env_steps=False)
check(rew, {"a0": 1, "a1": 3})
rew = episode.get_rewards(-2, env_steps=False)
check(rew, {"a0": 0, "a1": 2})
rew = episode.get_rewards(-3, env_steps=False, agent_ids="a1")
check(rew, {"a1": 1})
rew = episode.get_rewards(-3, env_steps=False, fill=-4)
check(rew, {"a0": -4, "a1": 1})
rew = episode.get_rewards(-4, env_steps=False, agent_ids="a1")
check(rew, {"a1": 0})
rew = episode.get_rewards(-4, env_steps=False, fill=-5)
check(rew, {"a0": -5, "a1": 0})
# Generate simple records for a multi-agent environment.
(
observations,
actions,
rewards,
is_terminateds,
is_truncateds,
infos,
) = self._mock_multi_agent_records()
# Create a multi-agent episode.
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
infos=infos,
terminateds=is_terminateds,
truncateds=is_truncateds,
len_lookback_buffer="auto", # default: use all data as lookback
)
# Get last rewards for the multi-agent episode.
rew = episode.get_rewards(indices=-1)
check(rew, {"agent_1": 1.1, "agent_3": 1.2, "agent_4": 1.3})
# Return last two rewards for the entire env.
# Also, we flip the indices here and require -1 before -2, this
# should reflect in the returned results.
rew = episode.get_rewards(indices=[-1, -2])
check(
rew,
{
"agent_1": [1.1, 0.5],
"agent_2": [0.6],
"agent_3": [1.2, 0.7],
"agent_4": [1.3],
},
)
# Return last two rewards for the entire env using slice.
rew = episode.get_rewards(slice(-2, None))
check(
rew,
{
"agent_1": [0.5, 1.1],
"agent_2": [0.6],
"agent_3": [0.7, 1.2],
"agent_4": [1.3],
},
)
# Return last four rewards for the entire env using slice
# and `fill`.
rew = episode.get_rewards(slice(-5, None), fill=-10)
check(
rew,
{
# All first three ts should be 0s (fill before episode even
# started).
# 4th items are the 1st rewards (after reset obs) for agents
"agent_1": [-10, -10, -10, 0.5, 1.1], # ag1 stepped the first 2 ts
"agent_2": [-10, -10, -10, 0.6, -10], # ag2 stepped first ts
"agent_3": [-10, -10, -10, 0.7, 1.2], # ag3 same as ag1
"agent_4": [-10, -10, -10, -10, 1.3], # ag4 steps in last 2 ts
},
)
# Use `fill` to look into the future (ts=100 and 101).
rew = episode.get_rewards(slice(100, 102), fill=9.9)
check(
rew,
{
"agent_1": [9.9, 9.9],
"agent_2": [9.9, 9.9],
"agent_3": [9.9, 9.9],
"agent_4": [9.9, 9.9],
},
)
# Return two rewards in lookback buffers for the entire env using
# `neg_index_as_lookback=True` and an index list.
# w/ fill
rew = episode.get_rewards(
indices=[-2, -1],
fill=-10,
neg_index_as_lookback=True,
)
check(
rew,
{
"agent_1": [0.5, 1.1],
"agent_2": [0.6, -10],
"agent_3": [0.7, 1.2],
"agent_4": [-10, 1.3],
},
)
# Same, but w/o fill.
episode.get_rewards(indices=[-2, -1], neg_index_as_lookback=True)
# Get last rewards for each individual agent.
rew = episode.get_rewards(indices=-1, env_steps=False)
check(rew, {"agent_1": 1.1, "agent_2": 0.6, "agent_3": 1.2, "agent_4": 1.3})
# Same, but with `agent_ids` filters.
rew = episode.get_rewards(-1, env_steps=False, agent_ids="agent_1")
check(rew, {"agent_1": 1.1})
rew = episode.get_rewards(-1, env_steps=False, agent_ids=["agent_2"])
check(rew, {"agent_2": 0.6})
rew = episode.get_rewards(-1, env_steps=False, agent_ids=("agent_3",))
check(rew, {"agent_3": 1.2})
rew = episode.get_rewards(-1, env_steps=False, agent_ids={"agent_4"})
check(rew, {"agent_4": 1.3})
rew = episode.get_rewards(-1, env_steps=False, agent_ids=["agent_1", "agent_2"])
check(rew, {"agent_1": 1.1, "agent_2": 0.6})
rew = episode.get_rewards(-2, env_steps=True, agent_ids={"agent_3"})
check(rew, {"agent_3": 0.7})
rew = episode.get_rewards(-2, env_steps=True, agent_ids={"agent_4"})
check(rew, {})
rew = episode.get_rewards([-1, -2], env_steps=True, agent_ids={"agent_3"})
check(rew, {"agent_3": [1.2, 0.7]})
rew = episode.get_rewards([-1, -2], env_steps=True, agent_ids={"agent_4"})
check(rew, {"agent_4": [1.3]})
# Agent 4 has only acted 2x, so there is no (local) ts=-2 for it.
with self.assertRaises(IndexError):
episode.get_rewards([-1, -2], env_steps=False, agent_ids={"agent_4"})
rew = episode.get_rewards([-2], env_steps=False, agent_ids="agent_4", fill=-10)
check(rew, {"agent_4": [-10]})
# Now, test the same when returning a list.
# B/c we have lookback="auto" in the ma episode, all data we sent into
# the c"tor was pushed into the lookback buffers and thus all
# rewards are in these buffers (and won't get returned here).
rew = episode.get_rewards(return_list=True)
self.assertTrue(rew == [])
# Expect error when calling with combination of env_steps=False, but
# return_list=True.
with self.assertRaises(ValueError):
episode.get_rewards(env_steps=False, return_list=True)
# List of indices.
rew = episode.get_rewards(indices=[-1, -2], return_list=True)
check(rew, [rewards[-1], rewards[-2]])
# Slice of indices w/ fill.
# From the last ts in lookback buffer to first actual ts (empty as all data is
# in lookback buffer).
rew = episode.get_rewards(
slice(-1, 1),
return_list=True,
fill=-8,
neg_index_as_lookback=True,
)
check(
rew,
[
{"agent_1": 1.1, "agent_2": -8, "agent_3": 1.2, "agent_4": 1.3},
{"agent_1": -8, "agent_2": -8, "agent_3": -8, "agent_4": -8},
],
)
# B/c we have lookback="auto" in the ma episode, all data we sent into
# the c"tor was pushed into the lookback buffers and thus all
# rewards are in these buffers.
rew = episode.get_rewards(env_steps=False)
self.assertTrue(rew == {})
# Test with initial rewards only.
episode = MultiAgentEpisode()
episode.add_env_reset(observations=observations[0], infos=infos[0])
# Get the last action for agents and assert that it's correct.
rew = episode.get_rewards()
check(rew, {})
# Now the same as list.
rew = episode.get_rewards(return_list=True)
self.assertTrue(rew == [])
# Now agent steps.
rew = episode.get_rewards(env_steps=False)
self.assertTrue(rew == {})
# Regression test for https://github.com/ray-project/ray/issues/62903
# get_rewards() on a finalized (numpy) episode should not crash when
# an agent was inactive during all requested env steps.
observations = [
{"a0": 0, "a1": 0}, # env step 0: both agents
{"a0": 1}, # env step 1: only a0 (a1 inactive)
{"a0": 2}, # env step 2: only a0 (a1 inactive)
{"a0": 3, "a1": 3}, # env step 3: both agents (terminal)
]
actions = [{"a0": 0, "a1": 0}, {"a0": 1}, {"a0": 2}]
rewards = [
{"a0": 0.1, "a1": 0.1},
{"a0": 0.2},
{"a0": 0.3},
{"a0": 0.4, "a1": 0.4},
]
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
len_lookback_buffer=0,
terminateds={"a0": True, "a1": True, "__all__": True},
)
episode.to_numpy()
# Full range: a1 has data at env steps 0 and 3, should work.
rew = episode.get_rewards()
check(rew, {"a0": [0.1, 0.2, 0.3], "a1": [0.1]})
# Slice covering only env steps where a1 was inactive.
# Before fix: ValueError("Input `list_of_structs` does not contain any items.")
rew = episode.get_rewards(indices=slice(1, 3))
check(rew, {"a0": [0.2, 0.3]})
self.assertNotIn("a1", rew)
# The fix is in the shared path _get_single_agent_data_by_env_step_indices,
# so get_actions should also work for the same slice.
act = episode.get_actions(indices=slice(1, 3))
check(act, {"a0": [1, 2]})
self.assertNotIn("a1", act)
# Non-finalized (list-based) episodes should behave the same way.
episode_list = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
len_lookback_buffer=0,
terminateds={"a0": True, "a1": True, "__all__": True},
)
rew_list = episode_list.get_rewards(indices=slice(1, 3))
check(rew_list, {"a0": [0.2, 0.3]})
self.assertNotIn("a1", rew_list)
def test_get_extra_model_outputs_hanging_val(self):
"""Tests that get_extra_model_outputs(key=...) correctly indexes hanging_val.
When an agent has hanging extra_model_outputs (action sent, next obs not
yet received), the cached value must be indexed by the requested key before
being passed to InfiniteLookbackBuffer.get(). Without the fix:
- Non-finalized (list-based): silently returns the entire dict instead of
the scalar value (data corruption).
- Finalized (numpy-based): crashes in tree.map_structure due to structure
mismatch between dict and scalar.
"""
def _make_episode():
"""Build a 2-agent episode where a0 has hanging extra outputs."""
episode = MultiAgentEpisode()
episode.add_env_reset(
observations={"a0": 0, "a1": 0},
infos={"a0": {}, "a1": {}},
)
# t=1: both agents act and get new obs.
episode.add_env_step(
observations={"a0": 1, "a1": 1},
actions={"a0": 10, "a1": 10},
rewards={"a0": 1.0, "a1": 1.0},
infos={"a0": {}, "a1": {}},
terminateds={"a0": False, "a1": False, "__all__": False},
truncateds={"a0": False, "a1": False, "__all__": False},
extra_model_outputs={
"a0": {"vf_preds": 0.5, "action_dist_inputs": 1.0},
"a1": {"vf_preds": 0.6, "action_dist_inputs": 1.1},
},
)
# t=2: both act, but only a1 gets a new obs.
# a0 acts but gets no obs -> hanging action + extra_model_outputs.
episode.add_env_step(
observations={"a1": 2},
actions={"a0": 20, "a1": 20},
rewards={"a0": 2.0, "a1": 2.0},
infos={"a1": {}},
terminateds={"a1": False, "__all__": False},
truncateds={"a1": False, "__all__": False},
extra_model_outputs={
"a0": {"vf_preds": 0.7, "action_dist_inputs": 1.2},
"a1": {"vf_preds": 0.8, "action_dist_inputs": 1.3},
},
)
return episode
# --- Non-finalized episode (list-based buffers) ---
episode = _make_episode()
self.assertIn("a0", episode._hanging_extra_model_outputs_end)
# List indices -> _get_single_agent_data_by_env_step_indices.
result = episode.get_extra_model_outputs(
key="vf_preds",
indices=[-1, -2],
env_steps=True,
)
self.assertIn("a0", result)
check(result["a0"], [0.7, 0.5])
check(result["a1"], [0.8, 0.6])
# Slice indices -> _get_single_agent_data_by_env_step_indices.
result = episode.get_extra_model_outputs(
key="vf_preds",
indices=slice(-2, None),
env_steps=True,
)
self.assertIn("a0", result)
check(result["a0"], [0.5, 0.7])
check(result["a1"], [0.6, 0.8])
# Single int index -> _get_single_agent_data_by_index.
result = episode.get_extra_model_outputs(
key="vf_preds",
indices=-1,
env_steps=True,
)
self.assertIn("a0", result)
check(result["a0"], 0.7)
check(result["a1"], 0.8)
# Control: agent_steps path (already correct).
result = episode.get_extra_model_outputs(
key="vf_preds",
indices=-1,
env_steps=False,
agent_ids="a0",
)
self.assertIn("a0", result)
check(result["a0"], 0.7)
# --- Finalized episode (numpy-based buffers via to_numpy()) ---
# On the finalized path, the un-indexed hanging_val dict causes a crash
# in tree.map_structure: "The two structures don't have the same nested
# structure" (ndarray vs dict).
episode_fin = _make_episode()
episode_fin.to_numpy()
result = episode_fin.get_extra_model_outputs(
key="vf_preds",
indices=[-1, -2],
env_steps=True,
)
self.assertIn("a0", result)
check(result["a0"], [0.7, 0.5])
result = episode_fin.get_extra_model_outputs(
key="vf_preds",
indices=-1,
env_steps=True,
)
self.assertIn("a0", result)
check(result["a0"], 0.7)
def test_other_getters(self):
# TODO (simon): Revisit this test and the MultiAgentEpisode.episode_concat API.
return
(
observations,
actions,
rewards,
is_terminateds,
is_truncateds,
infos,
) = self._mock_multi_agent_records()
# Define some extra model outputs.
extra_model_outputs = [
# Here agent_2 has to buffer.
{"agent_1": {"extra": 0}, "agent_2": {"extra": 0}, "agent_3": {"extra": 0}},
{"agent_1": {"extra": 1}, "agent_3": {"extra": 1}, "agent_4": {"extra": 1}},
]
# Create a multi-agent episode.
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
infos=infos,
terminateds=is_terminateds,
truncateds=is_truncateds,
# extra_model_outputs=extra_model_outputs,
# len_lookback_buffer=0,
)
# --- extra_model_outputs ---
last_extra_model_outputs = episode.get_extra_model_outputs("extra")
check(
last_extra_model_outputs["agent_1"][0],
extra_model_outputs[-1]["agent_1"]["extra"],
)
check(
last_extra_model_outputs["agent_3"][0],
extra_model_outputs[-1]["agent_3"]["extra"],
)
check(
last_extra_model_outputs["agent_4"][0],
extra_model_outputs[-1]["agent_4"]["extra"],
)
# Request the last two outputs.
last_extra_model_outputs = episode.get_extra_model_outputs(
"extra", indices=[-1, -2]
)
check(
last_extra_model_outputs["agent_1"][0],
extra_model_outputs[-1]["agent_1"]["extra"],
)
check(
last_extra_model_outputs["agent_3"][0],
extra_model_outputs[-1]["agent_3"]["extra"],
)
check(
last_extra_model_outputs["agent_4"][0],
extra_model_outputs[-1]["agent_4"]["extra"],
)
check(
last_extra_model_outputs["agent_1"][1],
extra_model_outputs[-2]["agent_1"]["extra"],
)
check(
last_extra_model_outputs["agent_2"][0],
extra_model_outputs[-2]["agent_2"]["extra"],
)
check(
last_extra_model_outputs["agent_3"][1],
extra_model_outputs[-2]["agent_3"]["extra"],
)
# Now request lists.
# last_extra_model_outputs = episode.get_extra_model_outputs(
# "extra", as_list=True
# )
# check(
# last_extra_model_outputs[0]["agent_1"],
# extra_model_outputs[-1]["agent_1"]["extra"],
# )
# check(
# last_extra_model_outputs[0]["agent_3"],
# extra_model_outputs[-1]["agent_3"]["extra"],
# )
# check(
# last_extra_model_outputs[0]["agent_4"],
# extra_model_outputs[-1]["agent_4"]["extra"],
# )
# Request the last two extra model outputs and return as a list.
# last_extra_model_outputs = episode.get_extra_model_outputs(
# "extra", [-1, -2], as_list=True
# )
# check(
# last_extra_model_outputs[0]["agent_1"],
# extra_model_outputs[-1]["agent_1"]["extra"],
# )
# check(
# last_extra_model_outputs[0]["agent_3"],
# extra_model_outputs[-1]["agent_3"]["extra"],
# )
# check(
# last_extra_model_outputs[0]["agent_4"],
# extra_model_outputs[-1]["agent_4"]["extra"],
# )
# check(
# last_extra_model_outputs[1]["agent_1"],
# extra_model_outputs[-2]["agent_1"]["extra"],
# )
# check(
# last_extra_model_outputs[1]["agent_2"],
# extra_model_outputs[-2]["agent_2"]["extra"],
# )
# check(
# last_extra_model_outputs[1]["agent_3"],
# extra_model_outputs[-2]["agent_3"]["extra"],
# )
# Now request the last extra model outputs at the local timesteps, i.e.
# for each agent its last two actions.
last_extra_model_outputs = episode.get_extra_model_outputs(
"extra", [-1, -2], env_steps=False
)
check(
last_extra_model_outputs["agent_1"][0],
extra_model_outputs[-1]["agent_1"]["extra"],
)
check(
last_extra_model_outputs["agent_3"][0],
extra_model_outputs[-1]["agent_3"]["extra"],
)
check(
last_extra_model_outputs["agent_4"][0],
extra_model_outputs[-1]["agent_4"]["extra"],
)
check(
last_extra_model_outputs["agent_1"][1],
extra_model_outputs[-2]["agent_1"]["extra"],
)
check(
last_extra_model_outputs["agent_2"][0],
extra_model_outputs[-2]["agent_2"]["extra"],
)
check(
last_extra_model_outputs["agent_3"][1],
extra_model_outputs[-2]["agent_3"]["extra"],
)
# TODO (simon): Not tested with `env_steps=False`.
# --- rewards ---
# Start with the case of no partial or buffered rewards.
last_rewards = episode.get_rewards(partial=False, consider_buffer=False)
self.assertTrue(
last_rewards["agent_4"][0], rewards[0]["agent_4"] + rewards[1]["agent_4"]
)
self.assertTrue(last_rewards["agent_2"][0], rewards[1]["agent_2"])
# Now test the same case, but with the last two rewards.
last_rewards = episode.get_rewards(
[-1, -2], partial=False, consider_buffer=False
)
self.assertTrue(
last_rewards["agent_4"][0], rewards[0]["agent_4"] + rewards[1]["agent_4"]
)
self.assertTrue(last_rewards["agent_2"][0], rewards[1]["agent_2"])
self.assertTrue(last_rewards["agent_1"][0], rewards[0]["agent_1"])
self.assertTrue(last_rewards["agent_3"][0], rewards[0]["agent_3"])
# Now request these rewards as list.
last_rewards = episode.get_rewards(
as_list=True, partial=False, consider_buffer=False
)
self.assertTrue(
last_rewards[0]["agent_4"], rewards[0]["agent_4"] + rewards[1]["agent_4"]
)
self.assertTrue(last_rewards[0]["agent_2"], rewards[1]["agent_2"])
# Now test the same case, but with the last two rewards.
last_rewards = episode.get_rewards(
[-1, -2], as_list=True, partial=False, consider_buffer=False
)
self.assertTrue(
last_rewards[0]["agent_4"], rewards[0]["agent_4"] + rewards[1]["agent_4"]
)
self.assertTrue(last_rewards[0]["agent_2"], rewards[1]["agent_2"])
self.assertTrue(last_rewards[1]["agent_1"], rewards[0]["agent_1"])
self.assertTrue(last_rewards[1]["agent_3"], rewards[0]["agent_3"])
# Create an environment.
env = MultiAgentTestEnv()
# Create an empty episode.
episode_1 = MultiAgentEpisode(agent_ids=env.agent_ids)
# Generate initial observation and info.
obs, info = env.reset(seed=42)
episode_1.add_env_reset(
observations=obs,
infos=info,
)
# Now, generate 100 samples.
for i in range(100):
action = {agent_id: i for agent_id in obs}
obs, reward, terminated, truncated, info = env.step(action)
episode_1.add_env_step(
observations=obs,
actions=action,
rewards=reward,
infos=info,
terminateds=terminated,
truncateds=truncated,
extra_model_outputs={agent_id: {"extra": 10} for agent_id in action},
)
# First, receive the last rewards without considering buffered values.
last_rewards = episode_1.get_rewards(partial=False, consider_buffer=False)
self.assertIn("agent_9", last_rewards)
check(episode_1.global_t_to_local_t["agent_9"][-1], 100)
check(episode_1.agent_episodes["agent_9"].rewards[-1], 1.0)
check(last_rewards["agent_9"][0], 1.0)
self.assertIn("agent_0", last_rewards)
check(episode_1.global_t_to_local_t["agent_0"][-1], 100)
check(episode_1.agent_episodes["agent_0"].rewards[-1], 1.0)
check(last_rewards["agent_0"][0], 1.0)
self.assertIn("agent_2", last_rewards)
check(episode_1.global_t_to_local_t["agent_2"][-1], 100)
check(episode_1.agent_episodes["agent_2"].rewards[-1], 1.0)
check(last_rewards["agent_2"][0], 1.0)
self.assertIn("agent_5", last_rewards)
check(episode_1.global_t_to_local_t["agent_5"][-1], 100)
check(episode_1.agent_episodes["agent_5"].rewards[-1], 1.0)
check(last_rewards["agent_5"][0], 1.0)
self.assertIn("agent_8", last_rewards)
check(episode_1.global_t_to_local_t["agent_8"][-1], 100)
check(episode_1.agent_episodes["agent_8"].rewards[-1], 1.0)
check(last_rewards["agent_8"][0], 1.0)
self.assertIn("agent_4", last_rewards)
check(episode_1.global_t_to_local_t["agent_4"][-1], 100)
check(episode_1.agent_episodes["agent_4"].rewards[-1], 1.0)
check(last_rewards["agent_4"][0], 1.0)
self.assertIn("agent_3", last_rewards)
check(episode_1.global_t_to_local_t["agent_3"][-1], 100)
# Agent 3 had a partial reward before the last recorded observation.
check(episode_1.agent_episodes["agent_3"].rewards[-1], 2.0)
check(last_rewards["agent_3"][0], 2.0)
# Assert that all the other agents are not in the returned rewards.
self.assertNotIn("agent_1", last_rewards)
self.assertNotIn("agent_6", last_rewards)
self.assertNotIn("agent_7", last_rewards)
# Now return the same as list.
last_rewards = episode_1.get_rewards(
partial=False, consider_buffer=False, as_list=True
)
self.assertIn("agent_9", last_rewards[0])
check(last_rewards[0]["agent_9"], 1.0)
self.assertIn("agent_0", last_rewards[0])
check(last_rewards[0]["agent_0"], 1.0)
self.assertIn("agent_2", last_rewards[0])
check(last_rewards[0]["agent_2"], 1.0)
self.assertIn("agent_5", last_rewards[0])
check(last_rewards[0]["agent_5"], 1.0)
self.assertIn("agent_8", last_rewards[0])
check(last_rewards[0]["agent_8"], 1.0)
self.assertIn("agent_4", last_rewards[0])
check(last_rewards[0]["agent_4"], 1.0)
self.assertIn("agent_3", last_rewards[0])
check(last_rewards[0]["agent_3"], 2.0)
# Assert that all the other agents are not in the returned rewards.
self.assertNotIn("agent_1", last_rewards)
self.assertNotIn("agent_6", last_rewards)
self.assertNotIn("agent_7", last_rewards)
# Now request the last two indices.
last_rewards = episode_1.get_rewards(
[-1, -2], partial=False, consider_buffer=False
)
self.assertIn("agent_9", last_rewards)
self.assertListEqual(episode_1.global_t_to_local_t["agent_9"][-2:], [99, 100])
self.assertListEqual(
episode_1.agent_episodes["agent_9"].rewards[-2:], last_rewards["agent_9"]
)
self.assertIn("agent_5", last_rewards)
self.assertListEqual(episode_1.global_t_to_local_t["agent_5"][-2:], [99, 100])
# Agent 5 has already died, so we need to convert back to list.
self.assertListEqual(
episode_1.agent_episodes["agent_5"].rewards[-2:],
last_rewards["agent_5"],
)
self.assertIn("agent_2", last_rewards)
self.assertListEqual(episode_1.global_t_to_local_t["agent_2"][-2:], [99, 100])
self.assertListEqual(
episode_1.agent_episodes["agent_2"].rewards[-1:-3:-1],
last_rewards["agent_2"],
)
# Agent 2 had no observation at `ts=98`, but partial rewards.
self.assertGreater(99, episode_1.global_t_to_local_t["agent_2"][-3])
# Ensure that for agent 2 there had been three partial rewards in between the
# observation at `ts=95` and the next at `ts=99`.
self.assertListEqual(
episode_1.partial_rewards_t["agent_2"][-4:-1], [96, 98, 99]
)
self.assertIn("agent_3", last_rewards)
# Agent 3 had no observation at `ts=99`.
self.assertListEqual(episode_1.global_t_to_local_t["agent_3"][-2:], [98, 100])
check(
episode_1.agent_episodes["agent_3"].rewards[-1], last_rewards["agent_3"][0]
)
# Ensure that there was a partial reward at `ts=99`.
self.assertListEqual(episode_1.partial_rewards_t["agent_3"][-2:], [99, 100])
self.assertIn("agent_4", last_rewards)
self.assertListEqual(episode_1.global_t_to_local_t["agent_4"][-2:], [99, 100])
self.assertListEqual(
episode_1.agent_episodes["agent_4"].rewards[-2:], last_rewards["agent_4"]
)
self.assertIn("agent_8", last_rewards)
# Ensure that the third-last observation is before `ts=98`.
self.assertListEqual(
episode_1.global_t_to_local_t["agent_8"][-3:], [97, 99, 100]
)
# Ensure also that at `ts=97` there was a reward.
self.assertListEqual(episode_1.partial_rewards_t["agent_8"][-3:-1], [98, 99])
self.assertListEqual([1.0, 2.0], last_rewards["agent_8"])
self.assertIn("agent_7", last_rewards)
# Agent 7 has no observation at `ts=100`, but at `ts=98`.
self.assertListEqual(episode_1.global_t_to_local_t["agent_7"][-2:], [98, 99])
check(
episode_1.agent_episodes["agent_7"].rewards[-1], last_rewards["agent_7"][0]
)
self.assertIn("agent_0", last_rewards)
self.assertListEqual(episode_1.global_t_to_local_t["agent_0"][-2:], [99, 100])
self.assertListEqual(
episode_1.agent_episodes["agent_0"].rewards[-2:], last_rewards["agent_0"]
)
self.assertNotIn("agent_1", last_rewards)
self.assertNotIn("agent_6", last_rewards)
# Now request the last two indices as list.
last_rewards = episode_1.get_rewards(
[-1, -2], partial=False, consider_buffer=False, as_list=True
)
self.assertIn("agent_9", last_rewards[0])
self.assertIn("agent_9", last_rewards[1])
check(
episode_1.agent_episodes["agent_9"].rewards[-1], last_rewards[0]["agent_9"]
)
check(
episode_1.agent_episodes["agent_9"].rewards[-2], last_rewards[1]["agent_9"]
)
self.assertIn("agent_5", last_rewards[0])
self.assertIn("agent_5", last_rewards[1])
check(
episode_1.agent_episodes["agent_5"].rewards[-1], last_rewards[0]["agent_5"]
)
check(
episode_1.agent_episodes["agent_5"].rewards[-2], last_rewards[1]["agent_5"]
)
self.assertIn("agent_2", last_rewards[0])
self.assertIn("agent_2", last_rewards[1])
check(
episode_1.agent_episodes["agent_2"].rewards[-1], last_rewards[0]["agent_2"]
)
check(3.0, last_rewards[1]["agent_2"])
# Agent 3 has only recorded rewards at `ts=100`.
self.assertIn("agent_3", last_rewards[0])
check(
episode_1.agent_episodes["agent_3"].rewards[-1], last_rewards[0]["agent_3"]
)
self.assertIn("agent_4", last_rewards[0])
self.assertIn("agent_4", last_rewards[1])
check(
episode_1.agent_episodes["agent_4"].rewards[-1], last_rewards[0]["agent_4"]
)
check(
episode_1.agent_episodes["agent_4"].rewards[-2], last_rewards[1]["agent_4"]
)
self.assertIn("agent_8", last_rewards[0])
self.assertIn("agent_8", last_rewards[1])
check(
episode_1.agent_episodes["agent_8"].rewards[-1], last_rewards[0]["agent_8"]
)
check(
episode_1.agent_episodes["agent_8"].rewards[-2], last_rewards[1]["agent_8"]
)
# Agent 7 has no observation at `ts=100`.
self.assertIn("agent_7", last_rewards[1])
check(
episode_1.agent_episodes["agent_7"].rewards[-1], last_rewards[1]["agent_7"]
)
self.assertIn("agent_0", last_rewards[0])
self.assertIn("agent_0", last_rewards[1])
check(
episode_1.agent_episodes["agent_0"].rewards[-1], last_rewards[0]["agent_0"]
)
check(
episode_1.agent_episodes["agent_0"].rewards[-2], last_rewards[1]["agent_0"]
)
self.assertNotIn("agent_1", last_rewards[0])
self.assertNotIn("agent_6", last_rewards[0])
self.assertNotIn("agent_1", last_rewards[1])
self.assertNotIn("agent_6", last_rewards[1])
# Second, get the last rewards with a single index, consider all partial
# rewards after the last recorded observation of an agent, i.e. set
# `consider_buffer` to `True`.
last_rewards = episode_1.get_rewards(partial=False, consider_buffer=True)
self.assertIn("agent_9", last_rewards)
check(
episode_1.agent_episodes["agent_9"].rewards[-1], last_rewards["agent_9"][0]
)
self.assertIn("agent_0", last_rewards)
check(
episode_1.agent_episodes["agent_0"].rewards[-1], last_rewards["agent_0"][0]
)
self.assertIn("agent_2", last_rewards)
check(
episode_1.agent_episodes["agent_2"].rewards[-1], last_rewards["agent_2"][0]
)
self.assertIn("agent_5", last_rewards)
check(
episode_1.agent_episodes["agent_5"].rewards[-1], last_rewards["agent_5"][0]
)
self.assertIn("agent_8", last_rewards)
check(
episode_1.agent_episodes["agent_8"].rewards[-1], last_rewards["agent_8"][0]
)
self.assertIn("agent_4", last_rewards)
check(
episode_1.agent_episodes["agent_4"].rewards[-1], last_rewards["agent_4"][0]
)
self.assertIn("agent_3", last_rewards)
# Agent 3 had a partial reward before the last recorded observation.
check(
episode_1.agent_episodes["agent_3"].rewards[-1], last_rewards["agent_3"][0]
)
# Agent 7 has a partial reward at `ts=100` after its last observation at
# `ts=99`.
self.assertIn("agent_7", last_rewards)
check(episode_1.partial_rewards_t["agent_7"][-1], 100)
check(episode_1.partial_rewards["agent_7"][-1], last_rewards["agent_7"][0])
# Assert that all the other agents are not in the returned rewards.
self.assertNotIn("agent_1", last_rewards)
self.assertNotIn("agent_6", last_rewards)
# Now request the last rewards as a list while considering the buffer.
last_rewards = episode_1.get_rewards(
partial=False, consider_buffer=True, as_list=True
)
self.assertIn("agent_9", last_rewards[0])
check(
episode_1.agent_episodes["agent_9"].rewards[-1], last_rewards[0]["agent_9"]
)
self.assertIn("agent_0", last_rewards[0])
check(
episode_1.agent_episodes["agent_0"].rewards[-1], last_rewards[0]["agent_0"]
)
self.assertIn("agent_2", last_rewards[0])
check(
episode_1.agent_episodes["agent_2"].rewards[-1], last_rewards[0]["agent_2"]
)
self.assertIn("agent_5", last_rewards[0])
check(
episode_1.agent_episodes["agent_5"].rewards[-1], last_rewards[0]["agent_5"]
)
self.assertIn("agent_8", last_rewards[0])
check(
episode_1.agent_episodes["agent_8"].rewards[-1], last_rewards[0]["agent_8"]
)
self.assertIn("agent_4", last_rewards[0])
check(
episode_1.agent_episodes["agent_4"].rewards[-1], last_rewards[0]["agent_4"]
)
self.assertIn("agent_3", last_rewards[0])
# Agent 3 had a partial reward before the last recorded observation.
check(
episode_1.agent_episodes["agent_3"].rewards[-1], last_rewards[0]["agent_3"]
)
# Agent 7 has a partial reward at `ts=100` after its last observation at
# `ts=99`.
self.assertIn("agent_7", last_rewards[0])
check(episode_1.partial_rewards["agent_7"][-1], last_rewards[0]["agent_7"])
# Assert that all the other agents are not in the returned rewards.
self.assertNotIn("agent_1", last_rewards[0])
self.assertNotIn("agent_6", last_rewards[0])
# Now request the last two indices and consider buffered partial rewards after
# the last observation.
last_rewards = episode_1.get_rewards(
[-1, -2], partial=False, consider_buffer=True
)
self.assertIn("agent_9", last_rewards)
self.assertListEqual(
episode_1.agent_episodes["agent_9"].rewards[-1:-3:-1],
last_rewards["agent_9"],
)
self.assertIn("agent_0", last_rewards)
self.assertListEqual(
episode_1.agent_episodes["agent_0"].rewards[-1:-3:-1],
last_rewards["agent_0"],
)
self.assertIn("agent_2", last_rewards)
self.assertListEqual(
episode_1.agent_episodes["agent_2"].rewards[-1:-3:-1],
last_rewards["agent_2"],
)
self.assertIn("agent_5", last_rewards)
# Agent 5 already died, so we need to convert to list first.
self.assertListEqual(
episode_1.agent_episodes["agent_5"].rewards[-1:-3:-1],
last_rewards["agent_5"],
)
self.assertIn("agent_8", last_rewards)
self.assertListEqual(
episode_1.agent_episodes["agent_8"].rewards[-1:-3:-1],
last_rewards["agent_8"],
)
self.assertIn("agent_4", last_rewards)
self.assertListEqual(
episode_1.agent_episodes["agent_4"].rewards[-1:-3:-1],
last_rewards["agent_4"],
)
# Nothing changes for agent 3 as it has an observation at the last requested
# timestep 100, but not at `ts=99`.
self.assertIn("agent_3", last_rewards)
check(
episode_1.agent_episodes["agent_3"].rewards[-1], last_rewards["agent_3"][0]
)
# The entries for agent 6 have changed now b/c it has partial rewards during the
# requested timesteps 100 and 99.
self.assertIn("agent_6", last_rewards)
self.assertListEqual(episode_1.global_t_to_local_t["agent_6"][-2:], [95, 98])
self.assertListEqual(episode_1.partial_rewards_t["agent_6"][-2:], [99, 100])
self.assertListEqual(
episode_1.partial_rewards["agent_6"][-2:], last_rewards["agent_6"]
)
# Entries for agent 7 also change b/c this agent has a partial reward at
# `ts=100` while it has no observation recorded at this timestep.
self.assertIn("agent_7", last_rewards)
self.assertListEqual(episode_1.global_t_to_local_t["agent_7"][-2:], [98, 99])
self.assertListEqual(episode_1.partial_rewards_t["agent_7"][-2:], [99, 100])
check(episode_1.partial_rewards["agent_7"][-1], last_rewards["agent_7"][0])
check(
episode_1.agent_episodes["agent_7"].rewards[-1], last_rewards["agent_7"][1]
)
# Assert that all the other agents are not in the returned rewards.
self.assertNotIn("agent_1", last_rewards)
# Now request the same indices with `consider_buffer=True` and return them as
# a list.
last_rewards = episode_1.get_rewards(
[-1, -2], partial=False, consider_buffer=True, as_list=True
)
self.assertIn("agent_9", last_rewards[0])
self.assertIn("agent_9", last_rewards[1])
check(
episode_1.agent_episodes["agent_9"].rewards[-1], last_rewards[0]["agent_9"]
)
check(
episode_1.agent_episodes["agent_9"].rewards[-2], last_rewards[1]["agent_9"]
)
self.assertIn("agent_0", last_rewards[0])
self.assertIn("agent_0", last_rewards[1])
check(
episode_1.agent_episodes["agent_0"].rewards[-1], last_rewards[0]["agent_0"]
)
check(
episode_1.agent_episodes["agent_0"].rewards[-2], last_rewards[1]["agent_0"]
)
self.assertIn("agent_2", last_rewards[0])
self.assertIn("agent_2", last_rewards[1])
check(
episode_1.agent_episodes["agent_2"].rewards[-1], last_rewards[0]["agent_2"]
)
check(
episode_1.agent_episodes["agent_2"].rewards[-2], last_rewards[1]["agent_2"]
)
self.assertIn("agent_5", last_rewards[0])
self.assertIn("agent_5", last_rewards[1])
check(
episode_1.agent_episodes["agent_5"].rewards[-1], last_rewards[0]["agent_5"]
)
check(
episode_1.agent_episodes["agent_5"].rewards[-2], last_rewards[1]["agent_5"]
)
self.assertIn("agent_8", last_rewards[0])
self.assertIn("agent_8", last_rewards[1])
check(
episode_1.agent_episodes["agent_8"].rewards[-1], last_rewards[0]["agent_8"]
)
check(
episode_1.agent_episodes["agent_8"].rewards[-2], last_rewards[1]["agent_8"]
)
self.assertIn("agent_4", last_rewards[0])
self.assertIn("agent_4", last_rewards[1])
check(
episode_1.agent_episodes["agent_4"].rewards[-1], last_rewards[0]["agent_4"]
)
check(
episode_1.agent_episodes["agent_4"].rewards[-2], last_rewards[1]["agent_4"]
)
# Nothing changes for agent 3 as it has an observation at the last requested
# timestep 100.
self.assertIn("agent_3", last_rewards[0])
self.assertNotIn("agent_3", last_rewards[1])
check(
episode_1.agent_episodes["agent_3"].rewards[-1], last_rewards[0]["agent_3"]
)
# The entries for agent 6 have changed now b/c it has partial rewards during the
# requested timesteps 100 and 99.
self.assertIn("agent_6", last_rewards[0])
self.assertIn("agent_6", last_rewards[1])
check(episode_1.partial_rewards["agent_6"][-1], last_rewards[0]["agent_6"])
check(episode_1.partial_rewards["agent_6"][-2], last_rewards[1]["agent_6"])
# Entries for agent 7 also change b/c this agent has a partial reward at
# `ts=100` while it has no observation recorded at this timestep.
self.assertIn("agent_7", last_rewards[0])
self.assertIn("agent_7", last_rewards[1])
check(episode_1.partial_rewards["agent_7"][-1], last_rewards[0]["agent_7"])
check(
episode_1.agent_episodes["agent_7"].rewards[-1], last_rewards[1]["agent_7"]
)
# Assert that all the other agents are not in the returned rewards.
self.assertNotIn("agent_1", last_rewards[0])
self.assertNotIn("agent_1", last_rewards[1])
# Third, request only partial rewards, i.e. rewards do not get buffered and
# added up.
last_rewards = episode_1.get_rewards(partial=True, consider_buffer=False)
self.assertIn("agent_9", last_rewards)
check(episode_1.partial_rewards_t["agent_9"][-1], 100)
check(episode_1.partial_rewards["agent_9"][-1], last_rewards["agent_9"][-1])
self.assertIn("agent_0", last_rewards)
check(episode_1.partial_rewards_t["agent_0"][-1], 100)
check(episode_1.partial_rewards["agent_0"][-1], last_rewards["agent_0"][-1])
self.assertIn("agent_2", last_rewards)
check(episode_1.partial_rewards_t["agent_2"][-1], 100)
check(episode_1.partial_rewards["agent_2"][-1], last_rewards["agent_2"][-1])
self.assertIn("agent_8", last_rewards)
check(episode_1.partial_rewards_t["agent_8"][-1], 100)
check(episode_1.partial_rewards["agent_8"][-1], last_rewards["agent_8"][-1])
self.assertIn("agent_4", last_rewards)
check(episode_1.partial_rewards_t["agent_4"][-1], 100)
check(episode_1.partial_rewards["agent_4"][-1], last_rewards["agent_4"][-1])
self.assertIn("agent_3", last_rewards)
check(episode_1.partial_rewards_t["agent_3"][-1], 100)
check(episode_1.partial_rewards["agent_3"][-1], last_rewards["agent_3"][-1])
self.assertIn("agent_6", last_rewards)
check(episode_1.partial_rewards_t["agent_6"][-1], 100)
check(episode_1.partial_rewards["agent_6"][-1], last_rewards["agent_6"][-1])
self.assertIn("agent_7", last_rewards)
check(episode_1.partial_rewards_t["agent_7"][-1], 100)
check(episode_1.partial_rewards["agent_7"][-1], last_rewards["agent_7"][-1])
# Assert that all the other agents are not in the returned rewards.
self.assertNotIn("agent_1", last_rewards)
# Now request all partial rewards at the last timestep and return them as
# a list.
last_rewards = episode_1.get_rewards(
partial=True, consider_buffer=False, as_list=True
)
self.assertIn("agent_9", last_rewards[0])
check(episode_1.partial_rewards["agent_9"][-1], last_rewards[0]["agent_9"])
self.assertIn("agent_0", last_rewards[0])
check(episode_1.partial_rewards["agent_0"][-1], last_rewards[0]["agent_0"])
self.assertIn("agent_2", last_rewards[0])
check(episode_1.partial_rewards["agent_2"][-1], last_rewards[0]["agent_2"])
self.assertIn("agent_8", last_rewards[0])
check(episode_1.partial_rewards["agent_8"][-1], last_rewards[0]["agent_8"])
self.assertIn("agent_4", last_rewards[0])
check(episode_1.partial_rewards["agent_4"][-1], last_rewards[0]["agent_4"])
self.assertIn("agent_3", last_rewards[0])
check(episode_1.partial_rewards["agent_3"][-1], last_rewards[0]["agent_3"])
self.assertIn("agent_6", last_rewards[0])
check(episode_1.partial_rewards["agent_6"][-1], last_rewards[0]["agent_6"])
self.assertIn("agent_7", last_rewards[0])
check(episode_1.partial_rewards["agent_7"][-1], last_rewards[0]["agent_7"])
# Assert that all the other agents are not in the returned rewards.
self.assertNotIn("agent_1", last_rewards[0])
# Request the last two indices, but consider only partial rewards.
last_rewards = episode_1.get_rewards(
[-1, -2], partial=True, consider_buffer=False
)
self.assertIn("agent_9", last_rewards)
self.assertListEqual(episode_1.partial_rewards_t["agent_9"][-2:], [99, 100])
check(episode_1.partial_rewards["agent_9"][-1:-3:-1], last_rewards["agent_9"])
self.assertIn("agent_0", last_rewards)
self.assertListEqual(episode_1.partial_rewards_t["agent_0"][-2:], [99, 100])
check(episode_1.partial_rewards["agent_0"][-1:-3:-1], last_rewards["agent_0"])
self.assertIn("agent_2", last_rewards)
self.assertListEqual(episode_1.partial_rewards_t["agent_2"][-2:], [99, 100])
check(episode_1.partial_rewards["agent_2"][-1:-3:-1], last_rewards["agent_2"])
self.assertIn("agent_8", last_rewards)
self.assertListEqual(episode_1.partial_rewards_t["agent_8"][-2:], [99, 100])
check(episode_1.partial_rewards["agent_8"][-1:-3:-1], last_rewards["agent_8"])
self.assertIn("agent_4", last_rewards)
self.assertListEqual(episode_1.partial_rewards_t["agent_4"][-2:], [99, 100])
check(episode_1.partial_rewards["agent_4"][-1:-3:-1], last_rewards["agent_4"])
self.assertIn("agent_3", last_rewards)
self.assertListEqual(episode_1.partial_rewards_t["agent_3"][-2:], [99, 100])
check(episode_1.partial_rewards["agent_3"][-1:-3:-1], last_rewards["agent_3"])
self.assertIn("agent_6", last_rewards)
self.assertListEqual(episode_1.partial_rewards_t["agent_6"][-2:], [99, 100])
check(episode_1.partial_rewards["agent_6"][-1:-3:-1], last_rewards["agent_6"])
self.assertIn("agent_7", last_rewards)
self.assertListEqual(episode_1.partial_rewards_t["agent_7"][-2:], [99, 100])
check(episode_1.partial_rewards["agent_7"][-1:-3:-1], last_rewards["agent_7"])
# Assert that all the other agents are not in the returned rewards.
self.assertNotIn("agent_1", last_rewards)
# At last, request the last two indices for only partial rewards and return
# them as list.
last_rewards = episode_1.get_rewards(
[-1, -2], partial=True, consider_buffer=False, as_list=True
)
self.assertIn("agent_9", last_rewards[0])
self.assertIn("agent_9", last_rewards[1])
check(episode_1.partial_rewards["agent_9"][-1], last_rewards[0]["agent_9"])
check(episode_1.partial_rewards["agent_9"][-2], last_rewards[1]["agent_9"])
self.assertIn("agent_0", last_rewards[0])
self.assertIn("agent_0", last_rewards[1])
check(episode_1.partial_rewards["agent_0"][-1], last_rewards[0]["agent_0"])
check(episode_1.partial_rewards["agent_0"][-2], last_rewards[1]["agent_0"])
self.assertIn("agent_2", last_rewards[0])
self.assertIn("agent_2", last_rewards[1])
check(episode_1.partial_rewards["agent_2"][-1], last_rewards[0]["agent_2"])
check(episode_1.partial_rewards["agent_2"][-2], last_rewards[1]["agent_2"])
self.assertIn("agent_8", last_rewards[0])
self.assertIn("agent_8", last_rewards[1])
check(episode_1.partial_rewards["agent_8"][-1], last_rewards[0]["agent_8"])
check(episode_1.partial_rewards["agent_8"][-2], last_rewards[1]["agent_8"])
self.assertIn("agent_4", last_rewards[0])
self.assertIn("agent_4", last_rewards[1])
check(episode_1.partial_rewards["agent_4"][-1], last_rewards[0]["agent_4"])
check(episode_1.partial_rewards["agent_4"][-2], last_rewards[1]["agent_4"])
self.assertIn("agent_3", last_rewards[0])
self.assertIn("agent_3", last_rewards[1])
check(episode_1.partial_rewards["agent_3"][-1], last_rewards[0]["agent_3"])
check(episode_1.partial_rewards["agent_3"][-2], last_rewards[1]["agent_3"])
self.assertIn("agent_6", last_rewards[0])
self.assertIn("agent_6", last_rewards[1])
check(episode_1.partial_rewards["agent_6"][-1], last_rewards[0]["agent_6"])
check(episode_1.partial_rewards["agent_6"][-2], last_rewards[1]["agent_6"])
self.assertIn("agent_7", last_rewards[0])
self.assertIn("agent_7", last_rewards[1])
check(episode_1.partial_rewards["agent_7"][-1], last_rewards[0]["agent_7"])
check(episode_1.partial_rewards["agent_7"][-2], last_rewards[1]["agent_7"])
# Assert that all the other agents are not in the returned rewards.
self.assertNotIn("agent_1", last_rewards[0])
self.assertNotIn("agent_1", last_rewards[1])
# Now, test with `global_ts=False`, i.e. on local level.
# Begin with `partial=False` and `consider_buffer=False`
# --- is_terminated, is_truncated ---
def test_cut(self):
# Simple multi-agent episode, in which all agents always step.
episode = self._create_simple_episode(
[
{"a0": 0, "a1": 0},
{"a0": 1, "a1": 1},
{"a0": 2, "a1": 2},
]
)
successor = episode.cut()
check(len(successor), 0)
check(successor.env_t_started, 2)
check(successor.env_t, 2)
check(successor.env_t_to_agent_t, {"a0": [2], "a1": [2]})
a0 = successor.agent_episodes["a0"]
a1 = successor.agent_episodes["a1"]
check((len(a0), len(a1)), (0, 0))
check((a0.t_started, a1.t_started), (2, 2))
check((a0.t, a1.t), (2, 2))
check((a0.observations, a1.observations), ([2], [2]))
check((a0.actions, a1.actions), ([], []))
check((a0.rewards, a1.rewards), ([], []))
check(successor._hanging_actions_end, {})
check(successor._hanging_rewards_end, {})
check(successor._hanging_extra_model_outputs_end, {})
# Multi-agent episode with lookback buffer, in which all agents always step.
episode = self._create_simple_episode(
[
{"a0": 0, "a1": 0},
{"a0": 1, "a1": 1},
{"a0": 2, "a1": 2},
{"a0": 3, "a1": 3},
],
len_lookback_buffer=2,
agent_t_started={"a0": 0, "a1": 0},
)
# Cut with lookback=0 argument (default).
successor = episode.cut()
check(len(successor), 0)
check(successor.env_t_started, 1)
check(successor.env_t, 1)
check(successor.env_t_to_agent_t, {"a0": [1], "a1": [1]})
a0 = successor.agent_episodes["a0"]
a1 = successor.agent_episodes["a1"]
check((len(a0), len(a1)), (0, 0))
check((a0.t_started, a1.t_started), (1, 1))
check((a0.t, a1.t), (1, 1))
check((a0.observations, a1.observations), ([3], [3]))
check((a0.actions, a1.actions), ([], []))
check((a0.rewards, a1.rewards), ([], []))
check(successor._hanging_actions_end, {})
check(successor._hanging_rewards_end, {})
check(successor._hanging_extra_model_outputs_end, {})
# Cut with lookback=2 argument.
successor = episode.cut(len_lookback_buffer=2)
check(len(successor), 0)
check(successor.env_t_started, 1)
check(successor.env_t, 1)
check(successor.env_t_to_agent_t["a0"].data, [-1, 0, 1])
check(successor.env_t_to_agent_t["a1"].data, [-1, 0, 1])
check(successor.env_t_to_agent_t["a0"].lookback, 2)
check(successor.env_t_to_agent_t["a1"].lookback, 2)
a0 = successor.agent_episodes["a0"]
a1 = successor.agent_episodes["a1"]
check((len(a0), len(a1)), (0, 0))
check((a0.t_started, a1.t_started), (1, 1))
check((a0.t, a1.t), (1, 1))
check((a0.observations, a1.observations), ([3], [3]))
check((a0.actions, a1.actions), ([], []))
check((a0.rewards, a1.rewards), ([], []))
check(successor._hanging_actions_end, {})
check(successor._hanging_rewards_end, {})
check(successor._hanging_extra_model_outputs_end, {})
# Multi-agent episode, in which one agent has a long sequence of not acting,
# but does receive (intermittent/hanging) rewards during this time.
observations = [
{"a0": 0, "a1": 0}, # 0
{"a0": 1}, # 1
{"a0": 2}, # 2
{"a0": 3}, # 3
]
episode = MultiAgentEpisode(
observations=observations,
actions=observations[:-1],
rewards=[
{"a0": 0.0, "a1": 0.0}, # 0
{"a0": 0.1, "a1": 0.1}, # 1
{"a0": 0.2, "a1": 0.2}, # 2
],
len_lookback_buffer=0,
agent_t_started={"a0": 0, "a1": 0},
)
successor = episode.cut()
check(len(successor), 0)
check(successor.env_t_started, 3)
check(successor.env_t, 3)
a0 = successor.agent_episodes["a0"]
self.assertTrue("a1" not in successor.agent_episodes)
check(len(a0), 0)
check(a0.t_started, 3)
check(a0.t, 3)
check(a0.observations, [3])
check(a0.actions, [])
check(a0.rewards, [])
check(successor._hanging_rewards_begin, {"a1": 0.3})
check(successor._hanging_actions_end, {})
check(successor._hanging_rewards_end, {"a1": 0.0})
check(successor._hanging_extra_model_outputs_end, {})
# Add a few timesteps to successor and test the resulting episode.
successor.add_env_step(
observations={"a0": 4},
actions={"a0": 3},
rewards={"a0": 0.3, "a1": 0.3},
)
check(len(successor), 1)
check(successor.env_t_started, 3)
check(successor.env_t, 4)
# Just b/c we added an intermittend reward for a1 does not mean it should
# already have a SAEps in `successor`. It still hasn't received its first obs
# yet after the cut.
self.assertTrue("a1" not in successor.agent_episodes)
check(len(a0), 1)
check(a0.t_started, 3)
check(a0.t, 4)
check(a0.observations, [3, 4])
check(a0.actions, [3])
check(a0.rewards, [0.3])
check(successor._hanging_rewards_begin, {"a1": 0.6})
check(successor._hanging_actions_end, {})
check(successor._hanging_rewards_end, {"a1": 0.0})
check(successor._hanging_extra_model_outputs_end, {})
# Now a1 actually does receive its next obs.
successor.add_env_step(
observations={"a0": 5, "a1": 5}, # <- this is a1's 1st obs in this chunk
actions={"a0": 4},
rewards={"a0": 0.4, "a1": 0.4},
)
check(len(successor), 2)
check(successor.env_t_started, 3)
check(successor.env_t, 5)
a1 = successor.agent_episodes["a1"]
check((len(a0), len(a1)), (2, 0))
check((a0.t_started, a1.t_started), (3, 0))
check((a0.t, a1.t), (5, 0))
check((a0.observations, a1.observations), ([3, 4, 5], [5]))
check((a0.actions, a1.actions), ([3, 4], []))
check((a0.rewards, a1.rewards), ([0.3, 0.4], []))
# Begin caches keep accumulating a1's rewards.
check(successor._hanging_rewards_begin, {"a1": 1.0})
# But end caches are now empty (due to a1's observation/finished step).
check(successor._hanging_actions_end, {})
check(successor._hanging_rewards_end, {"a1": 0.0})
check(successor._hanging_extra_model_outputs_end, {})
# Generate a simple multi-agent episode and check all internals after construction.
episode_1 = self._create_simple_episode(
[
{"a0": 0, "a1": 0},
{"a1": 1},
{"a1": 2},
{"a1": 3},
],
len_lookback_buffer="auto",
agent_t_started={"a0": 0, "a1": 3},
)
episode_2 = episode_1.cut()
check(episode_1.id_, episode_2.id_)
check(len(episode_1), 0)
check(len(episode_2), 0)
# Assert that all `SingleAgentEpisode`s have identical ids.
for agent_id, agent_eps in episode_2.agent_episodes.items():
check(agent_eps.id_, episode_1.agent_episodes[agent_id].id_)
# Assert that the timestep starts at the end of the last episode.
check(episode_1.env_t_started, 0)
check(episode_1.env_t, episode_2.env_t_started)
check(episode_2.env_t_started, episode_2.env_t)
# Make sure our mappings have been adjusted properly. We expect the mapping for
# a0 to have this agent's last obs added to the mapping's lookback buffer, such
# that we can add the buffered action to the new episode without problems.
check(episode_2.env_t_to_agent_t["a0"].data, [0, "S", "S", "S"])
check(episode_2.env_t_to_agent_t["a0"].lookback, 3)
check(episode_2.env_t_to_agent_t["a1"].data, [0, 1, 2, 3])
check(episode_2.env_t_to_agent_t["a1"].lookback, 3)
# Check all other internals of the cut episode chunk.
check(episode_2.agent_episodes["a0"].observations.data, [0])
check(episode_2.agent_episodes["a0"].observations.lookback, 0)
check(episode_2.agent_episodes["a0"].actions.data, [])
check(episode_2.agent_episodes["a0"].actions.lookback, 0)
# Test getting data from the cut chunk via the getter APIs.
check(episode_2.get_observations(-1), {"a1": 3})
check(episode_2.get_observations(-1, env_steps=False), {"a0": 0, "a1": 3})
check(episode_2.get_observations([-2, -1]), {"a1": [2, 3]})
check(episode_2.get_observations(slice(-3, None)), {"a1": [1, 2, 3]})
check(
episode_2.get_observations(slice(-4, None)), {"a0": [0], "a1": [0, 1, 2, 3]}
)
# Episode was just cut -> There can't be any actions in it yet (only in the
# lookback buffer).
check(episode_2.get_actions(), {})
check(episode_2.get_actions(-1), {"a1": 2})
check(episode_2.get_actions(-2), {"a1": 1})
check(episode_2.get_actions([-3]), {"a0": [0], "a1": [0]})
with self.assertRaises(IndexError):
episode_2.get_actions([-4])
# Don't expect index error if slice is given.
check(episode_2.get_actions(slice(-4, -3)), {})
episode_2.add_env_step(
actions={"a1": 4},
rewards={"a1": 0.4},
observations={"a0": 1, "a1": 4},
)
# Check everything again, but this time with the additional timestep taken.
check(len(episode_2), 1)
check(episode_2.env_t_to_agent_t["a0"].data, [0, "S", "S", "S", 1])
check(episode_2.env_t_to_agent_t["a0"].lookback, 3)
check(episode_2.env_t_to_agent_t["a1"].data, [0, 1, 2, 3, 4])
check(episode_2.env_t_to_agent_t["a1"].lookback, 3)
check(episode_2.agent_episodes["a0"].observations.data, [0, 1])
check(episode_2.agent_episodes["a0"].observations.lookback, 0)
# Action was "logged" -> Buffer should now be completely empty.
check(episode_2.agent_episodes["a0"].actions.data, [0])
check(episode_2._hanging_actions_end, {})
check(episode_2.agent_episodes["a0"].actions.lookback, 0)
check(episode_2.get_observations(-1), {"a0": 1, "a1": 4})
check(episode_2.get_observations(-1, env_steps=False), {"a0": 1, "a1": 4})
check(episode_2.get_observations([-2, -1]), {"a0": [1], "a1": [3, 4]})
check(episode_2.get_observations(slice(-3, None)), {"a0": [1], "a1": [2, 3, 4]})
check(
episode_2.get_observations(slice(-4, None)), {"a0": [1], "a1": [1, 2, 3, 4]}
)
# Episode was just cut -> There can't be any actions in it yet (only in the
# lookback buffer).
check(episode_2.get_actions(), {"a1": [4]})
check(episode_2.get_actions(-1), {"a1": 4})
check(episode_2.get_actions(-2), {"a1": 2})
check(episode_2.get_actions([-3]), {"a1": [1]})
check(episode_2.get_actions([-4]), {"a0": [0], "a1": [0]})
with self.assertRaises(IndexError):
episode_2.get_actions([-5])
# Don't expect index error if slice is given.
check(episode_2.get_actions(slice(-5, -4)), {})
# Create an environment.
episode_1, _ = self._mock_multi_agent_records_from_env(size=100)
# Assert that the episode has 100 timesteps.
check(episode_1.env_t, 100)
# Create a successor.
episode_2 = episode_1.cut()
# Assert that it has the same id.
check(episode_1.id_, episode_2.id_)
check(len(episode_1), 100)
check(len(episode_2), 0)
# Assert that all `SingleAgentEpisode`s have identical ids.
for agent_id, agent_eps in episode_2.agent_episodes.items():
check(agent_eps.id_, episode_1.agent_episodes[agent_id].id_)
# Assert that the timestep starts at the end of the last episode.
check(episode_1.env_t_started, 0)
check(episode_2.env_t, episode_2.env_t_started)
check(episode_1.env_t, episode_2.env_t_started)
# Another complex case.
episode = self._create_simple_episode(
[
{"a0": 0},
{"a2": 0},
{"a2": 1},
{"a2": 2},
{"a0": 1},
{"a2": 3},
{"a2": 4},
# <- BUT: actual cut here, b/c of hanging action of a2
{"a2": 5},
# <- would expect cut here (b/c of lookback==1)
{"a0": 2},
{"a1": 0},
],
len_lookback_buffer=0,
)
successor = episode.cut(len_lookback_buffer=1)
check(len(successor), 0)
check(successor.env_t, 9)
check(successor.env_t_started, 9)
self.assertTrue(all(len(e) == 0 for e in successor.agent_episodes.values()))
self.assertTrue(all(len(e) == 1 for e in successor.env_t_to_agent_t.values()))
self.assertTrue(
all(e.lookback == 2 for e in successor.env_t_to_agent_t.values())
)
check(successor.env_t_to_agent_t["a0"].data, ["S", 2, "S"])
check(successor.env_t_to_agent_t["a1"].data, ["S", "S", 0])
check(successor.env_t_to_agent_t["a2"].data, [5, "S", "S"])
check(successor.get_observations(0), {"a1": 0})
with self.assertRaises(IndexError):
successor.get_observations(1)
check(successor.get_observations(-2), {"a0": 2})
check(successor.get_observations(-3), {"a2": 5})
with self.assertRaises(IndexError):
successor.get_observations(-4)
# TODO (sven): Revisit this test and the MultiAgentEpisode.episode_concat API.
return
# Assert that the last observation and info of `episode_1` are the first
# observation and info of `episode_2`.
for agent_id, agent_obs in episode_1.get_observations(
-1, env_steps=False
).items():
# If agents are done only ensure that the `SingleAgentEpisode` does not
# exist in episode_2.
if episode_1.agent_episodes[agent_id].is_done:
self.assertTrue(agent_id not in episode_2.agent_episodes)
else:
check(
agent_obs,
episode_2.get_observations(
-1,
neg_index_as_lookback=True,
env_steps=False,
agent_ids=agent_id,
),
)
agent_infos = episode_1.get_infos(-1, env_steps=False)
check(
agent_infos,
episode_2.get_infos(0, agent_ids=agent_id),
)
# Now test the buffers.
for agent_id, agent_buffer in episode_1.agent_buffers.items():
# Make sure the action buffers are either both full or both empty.
check(
agent_buffer["actions"].full(),
episode_2.agent_buffers[agent_id]["actions"].full(),
)
# If the action buffers are full they should share the same value.
if agent_buffer["actions"].full():
check(
agent_buffer["actions"].queue[0],
episode_2.agent_buffers[agent_id]["actions"].queue[0],
)
# If the agent is not done, the buffers should be equal in value.
if not episode_1.agent_episodes[agent_id].is_done:
# The other buffers have default values, if the agent is not done.
# Note, reward buffers could be full of partial rewards.
check(
agent_buffer["rewards"].queue[0],
episode_2.agent_buffers[agent_id]["rewards"].queue[0],
)
# Here we want to know, if they are both different from `None`.
check(
agent_buffer["extra_model_outputs"].queue[0],
episode_2.agent_buffers[agent_id]["extra_model_outputs"].queue[0],
)
# If an agent is done the buffers should be empty for both, predecessor
# and successor.
else:
self.assertTrue(agent_buffer["actions"].empty())
self.assertTrue(agent_buffer["rewards"].empty())
self.assertTrue(agent_buffer["extra_model_outputs"].empty())
self.assertTrue(agent_buffer["actions"].empty())
self.assertTrue(agent_buffer["rewards"].empty())
self.assertTrue(agent_buffer["extra_model_outputs"].empty())
# Ensure that the timestep mappings match.
for agent_id, agent_global_ts in episode_2.global_t_to_local_t.items():
# If an agent is not done, we write over the timestep from its last
# observation.
if not episode_2.agent_episodes[agent_id].is_done:
check(agent_global_ts[0], episode_1.global_t_to_local_t[agent_id][-1])
# In the other case this mapping should be empty.
else:
check(len(agent_global_ts), 0)
# Assert that the global action timestep mappings match.
for agent_id, agent_global_ts in episode_2.global_actions_t.items():
# If an agent is not done, we write over the timestep from its last
# action.
if not episode_2.agent_episodes[agent_id].is_done:
# If a timestep mapping for actions was copied over the last timestep
# of the üredecessor and the first of the successor must match.
if agent_global_ts:
check(agent_global_ts[0], episode_1.global_actions_t[agent_id][-1])
# If no action timestep mapping was copied over the last action must
# have been at or before the last observation in the predecessor.
else:
self.assertGreaterEqual(
episode_1.global_t_to_local_t[agent_id][-1],
episode_1.global_actions_t[agent_id][-1],
)
# In the other case this mapping should be empty.
else:
check(len(agent_global_ts), 0)
# Assert that the partial reward mappings and histories match.
for agent_id, agent_global_ts in episode_2.partial_rewards_t.items():
# Ensure that timestep mapping and history have the same length.
check(len(agent_global_ts), len(episode_2.partial_rewards[agent_id]))
# If an agent is not done, we write over the timestep from its last
# partial rewards.
if not episode_2.agent_episodes[agent_id].is_done:
# If there are partial rewards after the last observation ensure
# they are correct.
if (
episode_1.global_t_to_local_t[agent_id][-1]
< episode_1.partial_rewards_t[agent_id][-1]
):
indices_after_last_obs = episode_1.partial_rewards_t[
agent_id
].find_indices_right(episode_1.global_t_to_local_t[agent_id][-1])
episode_1_partial_rewards = list(
map(
episode_1.partial_rewards[agent_id].__getitem__,
indices_after_last_obs,
)
)
check(
sum(episode_2.partial_rewards[agent_id]),
sum(episode_1_partial_rewards),
)
# Also ensure that the timestep mappings are correct.
episode_1_partial_rewards_t = list(
map(
episode_1.partial_rewards_t[agent_id].__getitem__,
indices_after_last_obs,
)
)
self.assertListEqual(
episode_2.partial_rewards_t[agent_id],
episode_1_partial_rewards_t,
)
# In the other case this mapping should be empty.
else:
check(len(agent_global_ts), 0)
# In the other case this mapping should be empty.
else:
check(len(agent_global_ts), 0)
# Now test, if the specific values in the buffers are correct.
(
observations,
actions,
rewards,
terminateds,
truncateds,
infos,
) = self._mock_multi_agent_records()
# Create the episode.
episode_1 = MultiAgentEpisode(
agent_ids=["agent_1", "agent_2", "agent_3", "agent_4", "agent_5"],
observations=observations,
actions=actions,
rewards=rewards,
infos=infos,
terminateds=terminateds,
truncateds=truncateds,
)
# Assert that agents 1 and 3's buffers are indeed full.
for agent_id in ["agent_1", "agent_3"]:
check(
actions[1][agent_id],
episode_1.agent_buffers[agent_id]["actions"].queue[0],
)
# # Put the action back into the buffer.
# episode_1.agent_buffers[agent_id]["actions"].put_nowait(
# actions[1][agent_id]
# )
# Now step once.
action = {"agent_2": 3, "agent_4": 3}
# This time agent 4 should have the buffer full, while agent 1 has emptied
# its buffer.
observation = {"agent_1": 3, "agent_2": 3}
# Agents 1 and 2 add the reward to its timestep, but agent 3 and agent 5
# add this to the buffer and to the global reward history.
reward = {"agent_1": 2.0, "agent_2": 2.0, "agent_3": 2.0, "agent_5": 2.0}
info = {"agent_1": {}, "agent_2": {}}
terminateds = {k: False for k in observation.keys()}
terminateds.update({"__all__": False})
truncateds = {k: False for k in observation.keys()}
truncateds.update({"__all__": False})
episode_1.add_env_step(
observations=observation,
actions=action,
rewards=reward,
infos=info,
terminateds=terminateds,
truncateds=truncateds,
)
# Check that the partial reward history is correct.
check(len(episode_1.partial_rewards_t["agent_5"]), 1)
check(len(episode_1.partial_rewards["agent_5"]), 1)
check(len(episode_1.partial_rewards_t["agent_3"]), 2)
check(len(episode_1.partial_rewards["agent_3"]), 2)
check(len(episode_1.partial_rewards_t["agent_2"]), 2)
check(len(episode_1.partial_rewards_t["agent_2"]), 2)
self.assertListEqual(episode_1.partial_rewards["agent_3"], [0.5, 2.0])
self.assertListEqual(episode_1.partial_rewards_t["agent_3"], [1, 3])
self.assertListEqual(episode_1.partial_rewards["agent_2"], [1.0, 2.0])
self.assertListEqual(episode_1.partial_rewards_t["agent_2"], [2, 3])
check(len(episode_1.partial_rewards["agent_4"]), 2)
self.assertListEqual(episode_1.partial_rewards["agent_4"], [0.5, 1.0])
self.assertListEqual(episode_1.partial_rewards_t["agent_4"], [1, 2])
# Now check that the reward buffers are full.
for agent_id in ["agent_3", "agent_5"]:
check(episode_1.agent_buffers[agent_id]["rewards"].queue[0], 2.0)
# Check that the reward history is correctly recorded.
check(episode_1.partial_rewards_t[agent_id][-1], episode_1.t)
check(episode_1.partial_rewards[agent_id][-1], 2.0)
# Now create the successor.
episode_2 = episode_1.cut()
for agent_id, agent_eps in episode_2.agent_episodes.items():
if len(agent_eps.observations) > 0:
# The successor's first observations should be the predecessor's last.
check(
agent_eps.observations[0],
episode_1.agent_episodes[agent_id].observations[-1],
)
# The successor's first entry in the timestep mapping should be the
# predecessor's last.
check(
episode_2.global_t_to_local_t[agent_id][
-1
], # + episode_2.t_started,
episode_1.global_t_to_local_t[agent_id][-1],
)
# Now test that the partial rewards fit.
for agent_id in ["agent_3", "agent_5"]:
check(len(episode_2.partial_rewards_t[agent_id]), 1)
check(episode_2.partial_rewards_t[agent_id][-1], 3)
check(episode_2.agent_buffers[agent_id]["rewards"].queue[0], 2.0)
# Assert that agent 3's and 4's action buffers are full.
self.assertTrue(episode_2.agent_buffers["agent_4"]["actions"].full())
self.assertTrue(episode_2.agent_buffers["agent_3"]["actions"].full())
# Also assert that agent 1's action b uffer was emptied with the last
# observations.
self.assertTrue(episode_2.agent_buffers["agent_1"]["actions"].empty())
def test_slice(self):
# Generate a simple multi-agent episode.
episode = self._create_simple_episode(
[
{"a0": 0, "a1": 0},
{"a1": 1},
{"a1": 2},
{"a0": 3, "a1": 3},
{"a0": 4},
{"a0": 5, "a1": 5},
{"a0": 6, "a1": 6},
{"a1": 7},
{"a1": 8},
{"a0": 9},
]
)
check(len(episode), 9)
# Slice the episode in different ways and check results.
# Empty slice.
slice_ = episode[100:100]
check(len(slice_), 0)
check(slice_.env_t_started, 9)
check(slice_.env_t, 9)
# All-include slices.
for s in [
slice(None, None, None),
slice(-100, None, None),
slice(None, 1000, None),
slice(-1000, 1000, None),
]:
slice_ = episode[s]
check(len(slice_), len(episode))
check(slice_.env_t_started, 0)
check(slice_.env_t, 9)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (5, 7))
check((a0.t_started, a1.t_started), (0, 0))
check((a0.t, a1.t), (5, 7))
check(
(a0.observations, a1.observations),
([0, 3, 4, 5, 6, 9], [0, 1, 2, 3, 5, 6, 7, 8]),
)
check((a0.actions, a1.actions), ([0, 3, 4, 5, 6], [0, 1, 2, 3, 5, 6, 7]))
check(
(a0.rewards, a1.rewards),
([0.0, 0.3, 0.4, 0.5, 0.6], [0.0, 0.1, 0.2, 0.3, 0.5, 0.6, 0.7]),
)
check((a0.is_done, a1.is_done), (False, False))
# From pos start.
slice_ = episode[2:]
check(len(slice_), 7)
check(slice_.env_t_started, 2)
check(slice_.env_t, 9)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (4, 5))
check((a0.t_started, a1.t_started), (1, 2))
check((a0.t, a1.t), (5, 7))
check(
(a0.observations, a1.observations),
([3, 4, 5, 6, 9], [2, 3, 5, 6, 7, 8]),
)
check((a0.actions, a1.actions), ([3, 4, 5, 6], [2, 3, 5, 6, 7]))
check(
(a0.rewards, a1.rewards),
([0.3, 0.4, 0.5, 0.6], [0.2, 0.3, 0.5, 0.6, 0.7]),
)
check((a0.is_done, a1.is_done), (False, False))
# If a slice ends in a "gap" for an agent, expect actions and rewards to be
# cached in the agent's buffer.
slice_ = episode[:1]
check(len(slice_), 1)
check(slice_.env_t_started, 0)
check(slice_.env_t, 1)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (0, 1))
check((a0.t_started, a1.t_started), (0, 0))
check((a0.t, a1.t), (0, 1))
check((a0.observations, a1.observations), ([0], [0, 1]))
check((a0.actions, a1.actions), ([], [0]))
check((a0.rewards, a1.rewards), ([], [0.0]))
check((a0.is_done, a1.is_done), (False, False))
check(slice_._hanging_actions_end["a0"], 0)
check(slice_._hanging_rewards_end["a0"], 0.0)
# To pos stop.
slice_ = episode[:3]
check(len(slice_), 3)
check(slice_.env_t_started, 0)
check(slice_.env_t, 3)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (1, 3))
check((a0.t_started, a1.t_started), (0, 0))
check((a0.t, a1.t), (1, 3))
check((a0.observations, a1.observations), ([0, 3], [0, 1, 2, 3]))
check((a0.actions, a1.actions), ([0], [0, 1, 2]))
check((a0.rewards, a1.rewards), ([0.0], [0.0, 0.1, 0.2]))
check((a0.is_done, a1.is_done), (False, False))
# To neg stop.
slice_ = episode[:-1]
check(len(slice_), 8)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (4, 7))
check((a0.t_started, a1.t_started), (0, 0))
check((a0.t, a1.t), (4, 7))
check(
(a0.observations, a1.observations),
([0, 3, 4, 5, 6], [0, 1, 2, 3, 5, 6, 7, 8]),
)
check((a0.actions, a1.actions), ([0, 3, 4, 5], [0, 1, 2, 3, 5, 6, 7]))
check(
(a0.rewards, a1.rewards),
([0.0, 0.3, 0.4, 0.5], [0.0, 0.1, 0.2, 0.3, 0.5, 0.6, 0.7]),
)
check((a0.is_done, a1.is_done), (False, False))
# Expect the hanging action to be found in the buffer.
check(slice_._hanging_actions_end["a0"], 6)
slice_ = episode[:-4]
check(len(slice_), 5)
check(slice_.env_t_started, 0)
check(slice_.env_t, 5)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (3, 4))
check((a0.t_started, a1.t_started), (0, 0))
check((a0.t, a1.t), (3, 4))
check((a0.observations, a1.observations), ([0, 3, 4, 5], [0, 1, 2, 3, 5]))
check((a0.actions, a1.actions), ([0, 3, 4], [0, 1, 2, 3]))
check(
(a0.rewards, a1.rewards),
([0.0, 0.3, 0.4], [0.0, 0.1, 0.2, 0.3]),
)
check((a0.is_done, a1.is_done), (False, False))
# From neg start.
slice_ = episode[-2:]
check(len(slice_), 2)
check(slice_.env_t_started, 7)
check(slice_.env_t, 9)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (0, 1))
check((a0.t_started, a1.t_started), (5, 6))
check((a0.t, a1.t), (5, 7))
check((a0.observations, a1.observations), ([9], [7, 8]))
check((a0.actions, a1.actions), ([], [7]))
check((a0.rewards, a1.rewards), ([], [0.7]))
check((a0.is_done, a1.is_done), (False, False))
# From neg start.
slice_ = episode[-3:]
check(len(slice_), 3)
check(slice_.env_t_started, 6)
check(slice_.env_t, 9)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (1, 2))
check((a0.t_started, a1.t_started), (4, 5))
check((a0.t, a1.t), (5, 7))
check((a0.observations, a1.observations), ([6, 9], [6, 7, 8]))
check((a0.actions, a1.actions), ([6], [6, 7]))
check((a0.rewards, a1.rewards), ([0.6], [0.6, 0.7]))
check((a0.is_done, a1.is_done), (False, False))
# From neg start.
slice_ = episode[-5:]
check(len(slice_), 5)
check(slice_.env_t_started, 4)
check(slice_.env_t, 9)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (3, 3))
check((a0.t_started, a1.t_started), (2, 4))
check((a0.t, a1.t), (5, 7))
check((a0.observations, a1.observations), ([4, 5, 6, 9], [5, 6, 7, 8]))
check((a0.actions, a1.actions), ([4, 5, 6], [5, 6, 7]))
check((a0.rewards, a1.rewards), ([0.4, 0.5, 0.6], [0.5, 0.6, 0.7]))
check((a0.is_done, a1.is_done), (False, False))
# From neg start to neg stop.
slice_ = episode[-4:-2]
check(len(slice_), 2)
check(slice_.env_t_started, 5)
check(slice_.env_t, 7)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (1, 2))
check((a0.t_started, a1.t_started), (3, 4))
check((a0.t, a1.t), (4, 6))
check((a0.observations, a1.observations), ([5, 6], [5, 6, 7]))
check((a0.actions, a1.actions), ([5], [5, 6]))
check((a0.rewards, a1.rewards), ([0.5], [0.5, 0.6]))
check((a0.is_done, a1.is_done), (False, False))
# Test what happens if one single-agent episode terminates earlier than the
# other.
observations = [
{"a0": 0, "a1": 0},
{"a0": 1, "a1": 1},
{"a1": 2},
{"a1": 3},
]
actions = [
{"a0": 0, "a1": 0},
{"a1": 1},
{"a1": 2},
]
rewards = [{aid: a / 10 for aid, a in a.items()} for a in actions]
# TODO (sven): Do NOT use self._create_simple_episode here b/c this util does
# not handle terminateds (should not create actions after final observations).
episode = MultiAgentEpisode(
observations=observations,
actions=actions,
rewards=rewards,
terminateds={"a0": True},
len_lookback_buffer=0,
)
# ---
slice_ = episode[:1]
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check(len(slice_), 1)
check(slice_.env_t_started, 0)
check(slice_.env_t, 1)
check((len(a0), len(a1)), (1, 1))
check((a0.t_started, a1.t_started), (0, 0))
check((a0.t, a1.t), (1, 1))
check((a0.observations, a1.observations), ([0, 1], [0, 1]))
check((a0.actions, a1.actions), ([0], [0]))
check((a0.rewards, a1.rewards), ([0.0], [0.0]))
check((a0.is_done, a1.is_done), (True, False))
# ---
slice_ = episode[:2]
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check(len(slice_), 2)
check(slice_.env_t_started, 0)
check(slice_.env_t, 2)
check((len(a0), len(a1)), (1, 2))
check((a0.t_started, a1.t_started), (0, 0))
check((a0.t, a1.t), (1, 2))
check((a0.observations, a1.observations), ([0, 1], [0, 1, 2]))
check((a0.actions, a1.actions), ([0], [0, 1]))
check((a0.rewards, a1.rewards), ([0.0], [0.0, 0.1]))
check((a0.is_done, a1.is_done), (True, False))
# ---
slice_ = episode[2:]
self.assertTrue("a0" not in slice_.agent_episodes)
a1 = slice_.agent_episodes["a1"]
check(len(slice_), 1)
check(slice_.env_t_started, 2)
check(slice_.env_t, 3)
check(len(a1), 1)
check(a1.t_started, 2)
check(a1.t, 3)
check(a1.observations, [2, 3])
check(a1.actions, [2])
check(a1.rewards, [0.2])
check(a1.is_done, False)
def test_slice_with_lookback(self):
# Test what happens if we have lookback buffers.
observations = [
{"a0": 0, "a1": 0}, # lookback -2
{"a0": 1, "a1": 1}, # lookback -1
{"a1": 2}, # 0
{"a1": 3}, # 1
{"a1": 4}, # 2
{"a0": 5, "a1": 5}, # 3
{"a0": 6}, # 4
{"a0": 7, "a1": 7}, # 5
{"a0": 8}, # 6
{"a1": 9}, # 7
]
# env-t 0 1 2 3 4 5 6 7 8 9
# a0 obs 0 1 5 6 7 8
# a1 obs 0 1 2 3 4 5 7 9
episode = self._create_simple_episode(
observations, len_lookback_buffer=2, agent_t_started={"a0": 2, "a1": 2}
)
# ---
slice_ = episode[1:3]
check(len(slice_), 2)
check(slice_.env_t_started, 1)
check(slice_.env_t, 3)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (0, 2))
check((a0.t_started, a1.t_started), (2, 3))
check((a0.t, a1.t), (2, 5))
check((a0.observations, a1.observations), ([5], [3, 4, 5]))
check((a0.actions, a1.actions), ([], [3, 4]))
check((a0.rewards, a1.rewards), ([], [0.3, 0.4]))
check((a0.is_done, a1.is_done), (False, False))
# ---
slice_ = episode[None:4]
check(len(slice_), 4)
check(slice_.env_t_started, 0)
check(slice_.env_t, 4)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (1, 3))
check((a0.t_started, a1.t_started), (2, 2))
check((a0.t, a1.t), (3, 5))
check((a0.observations, a1.observations), ([5, 6], [2, 3, 4, 5]))
check((a0.actions, a1.actions), ([5], [2, 3, 4]))
check((a0.rewards, a1.rewards), ([0.5], [0.2, 0.3, 0.4]))
check((a0.is_done, a1.is_done), (False, False))
# ---
slice_ = episode[-3:-1]
check(len(slice_), 2)
check(slice_.env_t_started, 4)
check(slice_.env_t, 6)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (2, 0))
check((a0.t_started, a1.t_started), (3, 6))
check((a0.t, a1.t), (5, 6))
check((a0.observations, a1.observations), ([6, 7, 8], [7]))
check((a0.actions, a1.actions), ([6, 7], []))
check((a0.rewards, a1.rewards), ([0.6, 0.7], []))
check((a0.is_done, a1.is_done), (False, False))
# ---
slice_ = episode[-1:None]
check(len(slice_), 1)
check(slice_.env_t_started, 6)
check(slice_.env_t, 7)
a0 = slice_.agent_episodes["a0"]
a1 = slice_.agent_episodes["a1"]
check((len(a0), len(a1)), (0, 0))
check((a0.t_started, a1.t_started), (5, 7))
check((a0.t, a1.t), (5, 7))
check((a0.observations, a1.observations), ([8], [9]))
check((a0.actions, a1.actions), ([], []))
check((a0.rewards, a1.rewards), ([], []))
check((a0.is_done, a1.is_done), (False, False))
def test_get_return(self):
# Generate an empty episode and ensure that the return is zero.
episode = MultiAgentEpisode()
# Now sample 100 timesteps.
episode, env = self._mock_multi_agent_records_from_env()
ret = episode.get_return()
# Ensure that the return is now at least zero.
self.assertGreaterEqual(ret, 0.0)
# Assert that the return is indeed the sum of all agent returns.
agent_returns = sum(
agent_eps.get_return() for agent_eps in episode.agent_episodes.values()
)
self.assertTrue(ret, agent_returns)
# Assert that adding the buffered rewards to the agent returns
# gives the expected result when considering the buffer in
# `get_return()`.
buffered_rewards = sum(episode._hanging_rewards_end.values())
self.assertTrue(
episode.get_return(include_hanging_rewards=True),
agent_returns + buffered_rewards,
)
def test_len(self):
# Generate an empty episode and ensure that `len()` raises an error.
episode = MultiAgentEpisode()
# Generate a new episode with some initialization data.
obs = [
{"a0": 0, "a1": 0},
{"a1": 1},
{"a0": 2},
{"a0": 3, "a1": 3},
]
episode = MultiAgentEpisode(
observations=obs, actions=obs[:-1], rewards=obs[:-1], len_lookback_buffer=0
)
check(len(episode), 3)
obs.append({"a1": 4})
episode = MultiAgentEpisode(
observations=obs, actions=obs[:-1], rewards=obs[:-1], len_lookback_buffer=0
)
check(len(episode), 4)
obs.append({"a0": 5, "a1": 5})
episode = MultiAgentEpisode(
observations=obs, actions=obs[:-1], rewards=obs[:-1], len_lookback_buffer=0
)
check(len(episode), 5)
obs.append({"a0": 6})
episode = MultiAgentEpisode(
observations=obs, actions=obs[:-1], rewards=obs[:-1], len_lookback_buffer=0
)
check(len(episode), 6)
# Create an episode and environment and sample 100 timesteps.
episode, env = self._mock_multi_agent_records_from_env()
# Assert that the length is indeed 100.
check(len(episode), 100)
# Now, build a successor.
successor = episode.cut()
# Sample another 100 timesteps.
successor, env = self._mock_multi_agent_records_from_env(
episode=successor, env=env, init=False
)
# Ensure that the length of the successor is 100.
self.assertTrue(len(successor), 100)
# Now concatenate the two episodes.
# episode.concat_episode(successor)
# Assert that the length is now 100.
# self.assertTrue(len(episode), 200)
def test_get_state_and_from_state(self):
# Generate an empty episode and ensure that the state is empty.
# Generate a simple multi-agent episode.
episode = self._create_simple_episode(
[
{"a0": 0, "a1": 0},
{"a1": 1},
{"a1": 2},
{"a0": 3, "a1": 3},
{"a0": 4},
{"a0": 5, "a1": 5},
{"a0": 6, "a1": 6},
{"a1": 7},
{"a1": 8},
{"a0": 9},
]
)
# Get the state of the episode.
state = episode.get_state()
# Ensure that the state is not empty.
self.assertTrue(state)
episode_2 = MultiAgentEpisode.from_state(state)
# Assert that the two episodes are identical.
self.assertEqual(episode_2.id_, episode.id_)
self.assertEqual(
episode_2.agent_to_module_mapping_fn, episode.agent_to_module_mapping_fn
)
self.assertEqual(
type(episode_2.observation_space), type(episode.observation_space)
)
self.assertEqual(type(episode_2.action_space), type(episode.action_space))
check(episode_2.env_t_started, episode.env_t_started)
check(episode_2.env_t, episode.env_t)
check(episode_2.agent_t_started, episode.agent_t_started)
self.assertEqual(episode_2.env_t_to_agent_t, episode.env_t_to_agent_t)
for agent_id, env_t_to_agent_t in episode_2.env_t_to_agent_t.items():
check(env_t_to_agent_t.data, episode.env_t_to_agent_t[agent_id].data)
check(
env_t_to_agent_t.lookback, episode.env_t_to_agent_t[agent_id].lookback
)
check(episode_2._hanging_actions_end, episode._hanging_actions_end)
check(
episode_2._hanging_extra_model_outputs_end,
episode._hanging_extra_model_outputs_end,
)
check(episode_2._hanging_rewards_end, episode._hanging_rewards_end)
check(episode_2._hanging_rewards_begin, episode._hanging_rewards_begin)
check(episode_2.is_terminated, episode.is_terminated)
check(episode_2.is_truncated, episode.is_truncated)
self.assertSetEqual(
set(episode_2.agent_episodes.keys()), set(episode.agent_episodes.keys())
)
for agent_id, agent_eps in episode_2.agent_episodes.items():
self.assertEqual(agent_eps.id_, episode.agent_episodes[agent_id].id_)
check(episode_2._start_time, episode._start_time)
check(episode_2._last_step_time, episode._last_step_time)
def test_get_sample_batch(self):
# TODO (simon): Revisit this test and the MultiAgentEpisode.episode_concat API.
return
# Generate an environment and episode and sample 100 timesteps.
episode, env = self._mock_multi_agent_records_from_env()
# Now convert to sample batch.
batch = episode.get_sample_batch()
# Assert that the timestep in the `MultiAgentBatch` is identical
# to the episode timestep.
check(len(batch), len(episode))
# Assert that all agents are present in the multi-agent batch.
# Note, all agents have collected samples.
for agent_id in episode.agent_ids:
self.assertTrue(agent_id in batch.policy_batches)
# Assert that the recorded history length is correct.
for agent_id, agent_eps in episode.agent_episodes.items():
check(len(agent_eps), len(batch[agent_id]))
# Assert that terminated agents are terminated in the sample batch.
for agent_id in ["agent_1", "agent_5"]:
self.assertTrue(batch[agent_id]["terminateds"][-1])
# Now test that when creating a successor its sample batch will
# contain the correct values.
successor = episode.cut()
# Run 100 more timesteps for the successor.
successor, env = self._mock_multi_agent_records_from_env(
episode=successor, env=env, init=False
)
# Convert this episode to a `MultiAgentBatch`.
batch = successor.get_sample_batch()
# Assert that the number of timesteps match between episode and batch.
# Note, the successor starts at `ts=100`.
check(len(batch), len(successor))
# Assert that all agents that were not done, yet, are present in the batch.
for agent_id in env._agents_alive:
self.assertTrue(agent_id in batch.policy_batches)
# Ensure that the timesteps for each agent matches the it's batch length.
for agent_id, agent_eps in successor.agent_episodes.items():
# Note, we take over agent_ids
if not agent_eps.is_done:
check(len(agent_eps), len(batch[agent_id]))
# Assert that now all agents are truncated b/c the environment truncated
# them.
for agent_id in batch.policy_batches:
self.assertTrue(batch[agent_id]["truncateds"][-1])
# Test now that when we concatenate the same logic holds.
episode.concat_episode(successor)
# Convert the concatenated episode to a sample batch now.
batch = episode.get_sample_batch()
# Assert that the length of episode and batch match.
check(len(batch), len(episode))
# Assert that all agents are present in the multi-agent batch.
# Note, in the concatenated episode - in contrast to the successor
# - we have all agents stepped.
for agent_id in episode.agent_ids:
self.assertTrue(agent_id in batch.policy_batches)
# Assert that the recorded history length is correct.
for agent_id, agent_eps in episode.agent_episodes.items():
check(len(agent_eps), len(batch[agent_id]))
# Assert that terminated agents are terminated in the sample batch.
for agent_id in ["agent_1", "agent_5"]:
self.assertTrue(batch[agent_id]["terminateds"][-1])
# Assert that all the other agents are truncated by the environment.
for agent_id in env._agents_alive:
self.assertTrue(batch[agent_id]["truncateds"][-1])
# Finally, test that an empty episode, gives an empty batch.
episode = MultiAgentEpisode(agent_ids=env.agents)
# Convert now to sample batch.
batch = episode.get_sample_batch()
# Ensure that this batch is empty.
check(len(batch), 0)
def _create_simple_episode(
self, obs, len_lookback_buffer: int = 0, agent_t_started: dict[str, int] = None
):
if agent_t_started is None:
unique_agents = {agent_id for ob in obs for agent_id in ob}
agent_t_started = {
agent_id: len_lookback_buffer for agent_id in unique_agents
}
return MultiAgentEpisode(
observations=obs,
actions=obs[:-1],
rewards=[{aid: o / 10 for aid, o in o_dict.items()} for o_dict in obs[:-1]],
len_lookback_buffer=len_lookback_buffer,
agent_t_started=agent_t_started,
)
def _mock_multi_agent_records_from_env(
self,
size: int = 100,
episode: MultiAgentEpisode = None,
env: gym.Env = None,
init: bool = True,
truncate: bool = True,
seed: Optional[int] = 42,
) -> Tuple[MultiAgentEpisode, gym.Env]:
# If the environment does not yet exist, create one.
env = env or MultiAgentTestEnv(truncate=truncate)
# If no episode is given, construct one.
# We give it the `agent_ids` to make it create all objects.
episode = MultiAgentEpisode() if episode is None else episode
# We initialize the episode, if requested.
if init:
obs, info = env.reset(seed=seed)
episode.add_env_reset(observations=obs, infos=info)
# In the other case we need at least the last observations for the next
# actions.
else:
obs = dict(episode.get_observations(-1))
# Sample `size` many records.
done_agents = {aid for aid, t in episode.get_terminateds().items() if t}
for i in range(env.t, env.t + size):
action = {
agent_id: i + 1 for agent_id in obs if agent_id not in done_agents
}
obs, reward, terminated, truncated, info = env.step(action)
done_agents |= {a for a, v in terminated.items() if v is True}
done_agents |= {a for a, v in truncated.items() if v is True}
episode.add_env_step(
observations=obs,
actions=action,
rewards=reward,
infos=info,
terminateds=terminated,
truncateds=truncated,
extra_model_outputs={agent_id: {"extra": 10} for agent_id in action},
)
# Return both, episode and environment.
return episode, env
@staticmethod
def _mock_multi_agent_records():
# Create some simple observations, actions, rewards, infos and
# extra model outputs.
observations = [
{"agent_1": 0, "agent_2": 0, "agent_3": 0},
# Here agent 2 is stepping, but does not receive a next
# observation.
{"agent_1": 1, "agent_3": 1, "agent_4": 1},
# Here agents 1 and 3 have stepped, but received no next
# observation. their actions should go into the buffers.
{"agent_2": 2, "agent_4": 2},
]
actions = [
# Here agent_2 has to buffer.
{"agent_1": 0, "agent_2": 0, "agent_3": 0},
{"agent_1": 1, "agent_3": 1, "agent_4": 1},
]
rewards = [
# Here agent 4 has to buffer the reward as does not have
# actions nor observation.
{"agent_1": 0.5, "agent_2": 0.6, "agent_3": 0.7},
# Agent 4 should now release the buffer with reward 1.0
# and add the next reward to it, as it stepped and received
# a next observation.
{"agent_1": 1.1, "agent_3": 1.2, "agent_4": 1.3},
]
infos = [
{"agent_1": {"a1_i0": 1}, "agent_2": {"a2_i0": 2}, "agent_3": {"a3_i0": 3}},
{
"agent_1": {"a1_i1": 1.1},
"agent_3": {"a3_i1": 3.1},
"agent_4": {"a4_i1": 4.1},
},
{"agent_2": {"a2_i2": 2.2}, "agent_4": {"a4_i2": 4.2}},
]
# Let no agent be terminated or truncated.
terminateds = {
"__all__": False,
"agent_1": False,
"agent_3": False,
"agent_4": False,
}
truncateds = {
"__all__": False,
"agent_1": False,
"agent_3": False,
"agent_4": False,
}
return observations, actions, rewards, terminateds, truncateds, infos
def test_setters(self):
"""Tests whether the MultiAgentEpisode's setter methods work as expected.
Also tests numpy'ized episodes.
This test covers all setter methods:
- set_observations
- set_actions
- set_rewards
- set_extra_model_outputs
Each setter is tested with various indexing scenarios including:
- Single index
- List of indices
- Slice objects
- Negative indices (both regular and lookback buffer interpretation)
Uses two agents: a0 and a1
"""
import copy
SOME_KEY = "some_key"
# Create a simple multi-agent episode with two agents without lookback buffer first for basic tests
episode = MultiAgentEpisode(
observations=[
{"a0": 100, "a1": 200}, # Initial observations
{"a0": 101, "a1": 201},
{"a0": 102, "a1": 202},
{"a0": 103, "a1": 203},
{"a0": 104, "a1": 204},
{"a0": 105, "a1": 205},
{"a0": 106, "a1": 206},
],
actions=[
{"a0": 1, "a1": 11},
{"a0": 2, "a1": 12},
{"a0": 3, "a1": 13},
{"a0": 4, "a1": 14},
{"a0": 5, "a1": 15},
{"a0": 6, "a1": 16},
],
rewards=[
{"a0": 0.1, "a1": 1.1},
{"a0": 0.2, "a1": 1.2},
{"a0": 0.3, "a1": 1.3},
{"a0": 0.4, "a1": 1.4},
{"a0": 0.5, "a1": 1.5},
{"a0": 0.6, "a1": 1.6},
],
extra_model_outputs=[
{"a0": {SOME_KEY: 0.01}, "a1": {SOME_KEY: 1.01}},
{"a0": {SOME_KEY: 0.02}, "a1": {SOME_KEY: 1.02}},
{"a0": {SOME_KEY: 0.03}, "a1": {SOME_KEY: 1.03}},
{"a0": {SOME_KEY: 0.04}, "a1": {SOME_KEY: 1.04}},
{"a0": {SOME_KEY: 0.05}, "a1": {SOME_KEY: 1.05}},
{"a0": {SOME_KEY: 0.06}, "a1": {SOME_KEY: 1.06}},
],
len_lookback_buffer=0,
)
test_patterns = [
# (description, new_data, indices)
("zero index", {"a0": 7353.0, "a1": 8353.0}, 0),
("single index", {"a0": 7353.0, "a1": 8353.0}, 2),
("negative index", {"a0": 7353.0, "a1": 8353.0}, -1),
("short list of indices", {"a0": [7353.0], "a1": [8353.0]}, [1]),
(
"long list of indices",
{"a0": [73.0, 53.0, 35.0, 53.0], "a1": [83.0, 63.0, 45.0, 63.0]},
[1, 2, 3, 4],
),
("short slice", {"a0": [7353.0], "a1": [8353.0]}, slice(2, 3)),
(
"long slice",
{"a0": [7.0, 3.0, 5.0, 3.0], "a1": [17.0, 13.0, 15.0, 13.0]},
slice(2, 6),
),
]
# Test setters with all patterns
numpy_episode = copy.deepcopy(episode).to_numpy()
for e in [episode, numpy_episode]:
print(f"Testing MultiAgent numpy'ized={e.is_numpy}...")
for desc, new_data, indices in test_patterns:
print(f"Testing MultiAgent {desc}...")
expected_data = new_data
test_new_data = new_data
# Convert lists to numpy arrays for numpy episodes
if e.is_numpy and isinstance(list(new_data.values())[0], list):
test_new_data = {
agent_id: np.array(agent_data)
for agent_id, agent_data in new_data.items()
}
# Test set_observations
e.set_observations(new_data=test_new_data, at_indices=indices)
result = e.get_observations(indices)
for agent_id in ["a0", "a1"]:
check(result[agent_id], expected_data[agent_id])
# Test set_actions
e.set_actions(new_data=test_new_data, at_indices=indices)
result = e.get_actions(indices)
for agent_id in ["a0", "a1"]:
check(result[agent_id], expected_data[agent_id])
# Test set_rewards
e.set_rewards(new_data=test_new_data, at_indices=indices)
result = e.get_rewards(indices)
for agent_id in ["a0", "a1"]:
check(result[agent_id], expected_data[agent_id])
# Test set_extra_model_outputs
# Note: We test this by directly checking the underlying agent episodes
# since get_extra_model_outputs can be complex with indices
e.set_extra_model_outputs(
key=SOME_KEY, new_data=test_new_data, at_indices=indices
)
# Verify that the setter worked by checking the individual agent episodes
if desc in ["single index", "zero index"]:
for agent_id in ["a0", "a1"]:
actual_idx = e.agent_episodes[agent_id].t_started + indices
if actual_idx < len(
e.agent_episodes[agent_id].get_extra_model_outputs(SOME_KEY)
):
check(
e.agent_episodes[agent_id].get_extra_model_outputs(
SOME_KEY
)[actual_idx],
expected_data[agent_id],
)
elif desc == "negative index":
for agent_id in ["a0", "a1"]:
agent_ep = e.agent_episodes[agent_id]
actual_idx = (
len(agent_ep.get_extra_model_outputs(SOME_KEY)) + indices
)
if actual_idx >= 0:
check(
agent_ep.get_extra_model_outputs(SOME_KEY)[actual_idx],
expected_data[agent_id],
)
elif desc in ["long list of indices", "short list of indices"]:
for agent_id in ["a0", "a1"]:
agent_ep = e.agent_episodes[agent_id]
for i, expected_val in enumerate(expected_data[agent_id]):
actual_idx = agent_ep.t_started + indices[i]
if actual_idx < len(
agent_ep.get_extra_model_outputs(SOME_KEY)
):
check(
agent_ep.get_extra_model_outputs(SOME_KEY)[
actual_idx
],
expected_val,
)
elif desc in ["long slice", "short slice"]:
for agent_id in ["a0", "a1"]:
agent_ep = e.agent_episodes[agent_id]
slice_indices = list(range(indices.start, indices.stop))
for i, expected_val in enumerate(expected_data[agent_id]):
actual_idx = agent_ep.t_started + slice_indices[i]
if actual_idx < len(
agent_ep.get_extra_model_outputs(SOME_KEY)
):
check(
agent_ep.get_extra_model_outputs(SOME_KEY)[
actual_idx
],
expected_val,
)
class MultiAgentCountingEnv(MultiAgentEnv):
def __init__(
self, agent_fns: dict[str, Callable[[int], bool]], max_episode_length: int = 100
):
super().__init__()
self.agents = list(agent_fns.keys())
self.possible_agents = list(agent_fns.keys())
self.agent_fns = agent_fns
self.observation_space = gym.spaces.Dict(
{
agent: gym.spaces.Discrete(max_episode_length)
for agent in self.possible_agents
}
)
self.action_space = gym.spaces.Dict(
{
agent: gym.spaces.Discrete(max_episode_length)
for agent in self.possible_agents
}
)
self.agent_timestep = {}
self.env_timestep = 0
self.max_episode_length = max_episode_length
# Precompute the last env_t where each agent will receive an observation
self.agent_last_obs_t = {}
for agent, fn in agent_fns.items():
for t in range(max_episode_length, -1, -1):
if fn(t):
self.agent_last_obs_t[agent] = t
break
def reset(
self,
*,
seed: Optional[int] = None,
options: Optional[dict] = None,
) -> Tuple[MultiAgentDict, MultiAgentDict]:
self.env_timestep = 0
self.agent_timestep = {agent: 0 for agent in self.possible_agents}
obs = self.get_obs()
return obs, {agent_id: {"env_timestep": self.env_timestep} for agent_id in obs}
def step(
self, action_dict: MultiAgentDict
) -> Tuple[
MultiAgentDict, MultiAgentDict, MultiAgentDict, MultiAgentDict, MultiAgentDict
]:
self.env_timestep += 1
obs = self.get_obs()
rewards = {agent: 1 for agent in obs.keys()}
info = {agent: {"env_timestep": self.env_timestep} for agent in obs.keys()}
# Terminate agents when this is their last observation
terminated = {
agent: self.env_timestep == self.agent_last_obs_t[agent]
for agent in obs.keys()
}
terminated["__all__"] = self.env_timestep == self.max_episode_length
return obs, rewards, terminated, {}, info
def get_obs(self) -> dict[str, int]:
obs = {}
for agent, fn in self.agent_fns.items():
if fn(self.env_timestep):
obs[agent] = self.agent_timestep[agent]
self.agent_timestep[agent] += 1
# Every timestep must have at least one observation
assert len(obs) > 0
return obs
class EchoRLModule(RLModule):
"""An RLModule that returns the observation as the action (for testing)."""
framework = "torch"
@override(RLModule)
def _forward(self, batch: Dict[str, Any], **kwargs) -> Dict[str, Any]:
"""Return the observation as the action."""
obs = batch[Columns.OBS]
# For Discrete observation space, obs is already an integer/array of integers
return {Columns.ACTIONS: obs}
@override(RLModule)
def _forward_inference(self, batch: Dict[str, Any], **kwargs) -> Dict[str, Any]:
return self._forward(batch, **kwargs)
@override(RLModule)
def _forward_exploration(self, batch: Dict[str, Any], **kwargs) -> Dict[str, Any]:
return self._forward(batch, **kwargs)
@override(RLModule)
def _forward_train(self, batch: Dict[str, Any], **kwargs) -> Dict[str, Any]:
raise NotImplementedError("EchoRLModule is not trainable!")
AGENT_FNS = {
"p_true": lambda x: True,
"p_mod_2": lambda x: x % 2 == 0,
"p_mod_3+": lambda x: x % 3 == 0 and x > 0,
"p_in": lambda x: x in [2, 12, 18, 19],
}
MAX_EPISODE_LENGTH = 20
# Sample 8 timesteps
# Env Time: 0 1 2 3 4 5 6 7 8 | 9 10 11 12 13 14 15 16 | 17 18 19 20 | 0 1 2 3
# Agents -------------------|------------------------|-------------|--------
# p_true : 0 1 2 3 4 5 6 7 8 | 9 10 11 12 13 14 15 16 | 17 18 19 20 | 0 1 2 3
# p_mod_2 : 0 - 1 - 2 - 3 - 4 | - 5 - 6 - 7 - 8 | - 9 - 10 | 0 - 1 -
# p_mod_3+: - - - 0 - - 1 - - | 2 - - 3 - - 4 - | - 5 - - | - - - 0
# p_in : - - 0 - - - - - - | - - - 1 - - - - | - 2 3 - | - - 0 -
CONFIG = (
PPOConfig()
.environment(
lambda cfg: MultiAgentCountingEnv(
AGENT_FNS, max_episode_length=MAX_EPISODE_LENGTH
)
)
.env_runners(
num_envs_per_env_runner=1,
num_env_runners=0,
)
.rl_module(rl_module_spec=RLModuleSpec(module_class=EchoRLModule))
.multi_agent(
policies={"p0"},
policy_mapping_fn=lambda aid, eps, **kw: "p0",
policies_to_train=[],
)
)
def test_multi_agent_episode_functionality(num_timesteps=8, num_samples=10):
"""This test checks that the core data returned from the interface between MAEnvRunner, MAEpisode and a MultiAgentEnv work as expected.
Using a counting environment with periodic agent observations and a custom echo RL-Module,
this allows us to check that the observations, rewards, actions match expectations.
In particular, this test has a focus on `env_t_to_agent_t` as this is used to understand
when and what observation align across episode chunks.
"""
env_runner = MultiAgentEnvRunner(CONFIG)
episodes = []
for repeat in range(num_samples):
new_episodes = env_runner.sample(
num_timesteps=num_timesteps, random_actions=False
)
episodes += new_episodes
# Add testing for individual episode chunks that the data is correct
for ep in new_episodes:
for agent_id, sa_episode in ep.agent_episodes.items():
obs = sa_episode.get_observations()
actions = sa_episode.get_actions()
rewards = sa_episode.get_rewards()
infos = sa_episode.get_infos()
env_t_to_agent_t = ep.env_t_to_agent_t[agent_id].get()
# The observation should be sequential for the sa_episode's length
assert list(obs) == list(range(sa_episode.t_started, sa_episode.t + 1))
# The action should mirror the observations (but one shorter due to initial obs)
assert list(actions) == list(range(sa_episode.t_started, sa_episode.t))
# The rewards should be same length as actions
assert list(rewards) == [1] * (sa_episode.t - sa_episode.t_started)
# The info should be the same length as observations
assert len(list(infos)) == len(list(obs))
# Check env_t_to_agent_t has data for every timestep inclusive
assert len(env_t_to_agent_t) == ep.env_t + 1 - ep.env_t_started
agent_t = sum(AGENT_FNS[agent_id](t) for t in range(ep.env_t_started))
expected_env_t_to_agent_t = []
for env_t in range(ep.env_t_started, ep.env_t + 1):
if AGENT_FNS[agent_id](env_t):
expected_env_t_to_agent_t.append(agent_t)
agent_t += 1
else:
expected_env_t_to_agent_t.append(
MultiAgentEpisode.SKIP_ENV_TS_TAG
)
assert list(env_t_to_agent_t) == expected_env_t_to_agent_t
# The info should contain the env_t of the observations
# This is equal to the env_t of the non-skip timesteps
non_skip_env_t = [
ep.env_t_started + idx
for idx, agent_t in enumerate(env_t_to_agent_t)
if agent_t != MultiAgentEpisode.SKIP_ENV_TS_TAG
]
if len(non_skip_env_t) < len(obs):
first_obs_env_t = next(
(
env_t
for env_t in range(ep.env_t_started, -1, -1)
if AGENT_FNS[agent_id](env_t)
)
)
non_skip_env_t = [first_obs_env_t] + non_skip_env_t
info_timesteps = [info["env_timestep"] for info in infos]
assert non_skip_env_t == info_timesteps
# Concatenate chunks together then test that the concatenated data is correct
unique_episode_ids = {eps.id_ for eps in episodes}
for ep_id in unique_episode_ids:
eps_chunks = [ep for ep in episodes if ep.id_ == ep_id]
# Concatenate episode chunks together
combined = eps_chunks[0]
for chunk in eps_chunks[1:]:
combined.concat_episode(chunk)
# Check the episode contents for each agent
for agent_id, sa_episode in combined.agent_episodes.items():
obs = sa_episode.get_observations()
actions = sa_episode.get_actions()
rewards = sa_episode.get_rewards()
infos = sa_episode.get_infos()
env_t_to_agent_t = combined.env_t_to_agent_t[agent_id].get()
# Observations should be sequential: 0, 1, 2, 3, ...
expected_obs = list(range(len(obs)))
assert list(obs) == expected_obs
# Actions should equal observations (EchoRLModule)
assert list(actions) == list(obs[:-1])
# Rewards should equal 1 for every timestep
assert list(rewards) == [1] * len(actions)
# You should have the same number of info as obs
assert len(list(infos)) == len(list(obs))
# For the env_t_to_agent_t, we should have data for each timestep
assert len(env_t_to_agent_t) == combined.env_t + 1
expected_env_t_to_agent_t = []
agent_t = 0
for env_t in range(combined.env_t + 1):
if AGENT_FNS[agent_id](env_t):
expected_env_t_to_agent_t.append(agent_t)
agent_t += 1
else:
expected_env_t_to_agent_t.append(MultiAgentEpisode.SKIP_ENV_TS_TAG)
assert list(env_t_to_agent_t) == expected_env_t_to_agent_t
# The info timesteps should equal to the non-skip timesteps
non_skip_agent_t = [
env_t
for env_t, agent_t in enumerate(env_t_to_agent_t)
if agent_t != MultiAgentEpisode.SKIP_ENV_TS_TAG
]
info_timesteps = [info["env_timestep"] for info in infos]
assert non_skip_agent_t == info_timesteps
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))