# flake8: noqa import copy # __rllib-sa-episode-01-begin__ from ray.rllib.env.single_agent_episode import SingleAgentEpisode # Construct a new episode (without any data in it yet). episode = SingleAgentEpisode() assert len(episode) == 0 episode.add_env_reset(observation="obs_0", infos="info_0") # Even with the initial obs/infos, the episode is still considered len=0. assert len(episode) == 0 # Fill the episode with some fake data (5 timesteps). for i in range(5): episode.add_env_step( observation=f"obs_{i+1}", action=f"act_{i}", reward=f"rew_{i}", terminated=False, truncated=False, infos=f"info_{i+1}", ) assert len(episode) == 5 # __rllib-sa-episode-01-end__ # __rllib-sa-episode-02-begin__ # We can now access information from the episode via its getter APIs. from ray.rllib.utils.test_utils import check # Get the very first observation ("reset observation"). Note that a single observation # is returned here (not a list of size 1 or a batch of size 1). check(episode.get_observations(0), "obs_0") # ... which is the same as using the indexing operator on the Episode's # `observations` property: check(episode.observations[0], "obs_0") # You can also get several observations at once by providing a list of indices: check(episode.get_observations([1, 2]), ["obs_1", "obs_2"]) # .. or a slice of observations by providing a python slice object: check(episode.get_observations(slice(1, 3)), ["obs_1", "obs_2"]) # Note that when passing only a single index, a single item is returned. # Whereas when passing a list of indices or a slice, a list of items is returned. # Similarly for getting rewards: # Get the last reward. check(episode.get_rewards(-1), "rew_4") # ... which is the same as using the slice operator on the `rewards` property: check(episode.rewards[-1], "rew_4") # Similarly for getting actions: # Get the first action in the episode (single item, not batched). # This works regardless of the action space. check(episode.get_actions(0), "act_0") # ... which is the same as using the indexing operator on the `actions` property: check(episode.actions[0], "act_0") # Finally, you can slice the entire episode using the []-operator with a slice notation: sliced_episode = episode[3:4] check(list(sliced_episode.observations), ["obs_3", "obs_4"]) check(list(sliced_episode.actions), ["act_3"]) check(list(sliced_episode.rewards), ["rew_3"]) # __rllib-sa-episode-02-end__ import copy # noqa episode_2 = copy.deepcopy(episode) # __rllib-sa-episode-03-begin__ # Episodes start in the non-numpy'ized state (in which data is stored # under the hood in lists). assert episode.is_numpy is False # Call `to_numpy()` to convert all stored data from lists of individual (possibly # complex) items to numpy arrays. Note that RLlib normally performs this method call, # so users don't need to call `to_numpy()` themselves. episode.to_numpy() assert episode.is_numpy is True # __rllib-sa-episode-03-end__ episode = episode_2 # __rllib-sa-episode-04-begin__ # An ongoing episode (of length 5): assert len(episode) == 5 assert episode.is_done is False # During an `EnvRunner.sample()` rollout, when enough data has been collected into # one or more Episodes, the `EnvRunner` calls the `cut()` method, interrupting # the ongoing Episode and returning a new continuation chunk (with which the # `EnvRunner` can continue collecting data during the next call to `sample()`): continuation_episode = episode.cut() # The length is still 5, but the length of the continuation chunk is 0. assert len(episode) == 5 assert len(continuation_episode) == 0 # Thanks to the lookback buffer, we can still access the most recent observation # in the continuation chunk: check(continuation_episode.get_observations(-1), "obs_5") # __rllib-sa-episode-04-end__ # __rllib-sa-episode-05-begin__ # Construct a new episode (with some data in its lookback buffer). episode = SingleAgentEpisode( observations=["o0", "o1", "o2", "o3"], actions=["a0", "a1", "a2"], rewards=[0.0, 1.0, 2.0], len_lookback_buffer=3, ) # Since our lookback buffer is 3, all data already specified in the constructor should # now be in the lookback buffer (and not be part of the `episode` chunk), meaning # the length of `episode` should still be 0. assert len(episode) == 0 # .. and trying to get the first reward will hence lead to an IndexError. try: episode.get_rewards(0) except IndexError: pass # Get the last 3 rewards (using the lookback buffer). check(episode.get_rewards(slice(-3, None)), [0.0, 1.0, 2.0]) # Assuming the episode actually started with `obs_0` (reset obs), # then `obs_1` + `act_0` + reward=0.0, but your model always requires a 1D reward tensor # of shape (5,) with the 5 most recent rewards in it. # You could try to code for this by manually filling the missing 2 timesteps with zeros: last_5_rewards = [0.0, 0.0] + episode.get_rewards(slice(-3, None)) # However, this will become extremely tedious, especially when moving to (possibly more # complex) observations and actions. # Instead, `SingleAgentEpisode` getters offer some useful options to solve this problem: last_5_rewards = episode.get_rewards(slice(-5, None), fill=0.0) # Note that the `fill` argument allows you to even go further back into the past, provided # you are ok with filling timesteps that are not covered by the lookback buffer with # a fixed value. # __rllib-sa-episode-05-end__ # __rllib-sa-episode-06-begin__ # Construct a new episode (len=3 and lookback buffer=3). episode = SingleAgentEpisode( observations=[ "o-3", "o-2", "o-1", # <- lookback # noqa "o0", "o1", "o2", "o3", # <- actual episode data # noqa ], actions=[ "a-3", "a-2", "a-1", # <- lookback # noqa "a0", "a1", "a2", # <- actual episode data # noqa ], rewards=[ -3.0, -2.0, -1.0, # <- lookback # noqa 0.0, 1.0, 2.0, # <- actual episode data # noqa ], len_lookback_buffer=3, ) assert len(episode) == 3 # In case you want to loop through global timesteps 0 to 2 (timesteps -3, -2, and -1 # being the lookback buffer) and at each such global timestep look 2 timesteps back, # you can do so easily using the `neg_index_as_lookback` arg like so: for global_ts in [0, 1, 2]: rewards = episode.get_rewards( slice(global_ts - 2, global_ts + 1), # Switch behavior of negative indices from "from-the-end" to # "into the lookback buffer": neg_index_as_lookback=True, ) print(rewards) # The expected output should be: # [-2.0, -1.0, 0.0] # global ts=0 (plus looking back 2 ts) # [-1.0, 0.0, 1.0] # global ts=1 (plus looking back 2 ts) # [0.0, 1.0, 2.0] # global ts=2 (plus looking back 2 ts) # __rllib-sa-episode-06-end__ # Looking back from ts=1, get the previous 4 rewards AND fill with 0.0 # in case we go over the beginning (ts=0). So we would expect # [0.0, 0.0, 0.0, r0] to be returned here, where r0 is the very first received # reward in the episode: episode.get_rewards(slice(-4, 0), neg_index_as_lookback=True, fill=0.0) # Note the use of fill=0.0 here (fill everything that's out of range with this # value) AND the argument `neg_index_as_lookback=True`, which interprets # negative indices as being left of ts=0 (e.g. -1 being the timestep before # ts=0). import gymnasium as gym import numpy as np # Assuming we had a complex action space (nested gym.spaces.Dict) with one or # more elements being Discrete or MultiDiscrete spaces: # 1) The `fill=...` argument would still work, filling all spaces (Boxes, # Discrete) with that provided value. # 2) Setting the flag `one_hot_discrete=True` would convert those discrete # sub-components automatically into one-hot (or multi-one-hot) tensors. # This simplifies the task of having to provide the previous 4 (nested and # partially discrete/multi-discrete) actions for each timestep within a training # batch, thereby filling timesteps before the episode started with 0.0s and # one-hot'ing the discrete/multi-discrete components in these actions: episode = SingleAgentEpisode( action_space=gym.spaces.Dict( { "a": gym.spaces.Discrete(3), "b": gym.spaces.MultiDiscrete([2, 3]), "c": gym.spaces.Box(-1.0, 1.0, (2,)), } ) ) # ... fill episode with data ... episode.add_env_reset(observation=0) # ... from a few steps. episode.add_env_step( observation=1, action={"a": 0, "b": np.array([1, 2]), "c": np.array([0.5, -0.5], np.float32)}, reward=1.0, ) # In your connector prev_4_a = [] # Note here that len(episode) does NOT include the lookback buffer. for ts in range(len(episode)): prev_4_a.append( episode.get_actions( indices=slice(ts - 4, ts), # Make sure negative indices are interpreted as # "into lookback buffer" neg_index_as_lookback=True, # Zero-out everything even further before the lookback buffer. fill=0.0, # Take care of discrete components (get ready as NN input). one_hot_discrete=True, ) ) # Finally, convert from list of batch items to a struct (same as action space) # of batched (numpy) arrays, in which all leafs have B==len(prev_4_a). from ray.rllib.utils.spaces.space_utils import batch prev_4_actions_col = batch(prev_4_a)