chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
# flake8: noqa
# __rllib-adv_api_counter_begin__
import ray
@ray.remote
class Counter:
def __init__(self):
self.count = 0
def inc(self, n):
self.count += n
def get(self):
return self.count
# on the driver
counter = Counter.options(name="global_counter").remote()
print(ray.get(counter.get.remote())) # get the latest count
# in your envs
counter = ray.get_actor("global_counter")
counter.inc.remote(1) # async call to increment the global count
# __rllib-adv_api_counter_end__
# __rllib-adv_api_explore_begin__
from ray.rllib.algorithms.algorithm_config import AlgorithmConfig
config = AlgorithmConfig().env_runners(
exploration_config={
# Special `type` key provides class information
"type": "StochasticSampling",
# Add any needed constructor args here.
"constructor_arg": "value",
}
)
# __rllib-adv_api_explore_end__
# __rllib-adv_api_evaluation_1_begin__
from ray.rllib.algorithms.algorithm_config import AlgorithmConfig
# Run one evaluation step on every 3rd `Algorithm.train()` call.
config = AlgorithmConfig().evaluation(
evaluation_interval=3,
)
# __rllib-adv_api_evaluation_1_end__
# __rllib-adv_api_evaluation_2_begin__
# Every time we run an evaluation step, run it for exactly 10 episodes.
config = AlgorithmConfig().evaluation(
evaluation_duration=10,
evaluation_duration_unit="episodes",
)
# Every time we run an evaluation step, run it for (close to) 200 timesteps.
config = AlgorithmConfig().evaluation(
evaluation_duration=200,
evaluation_duration_unit="timesteps",
)
# __rllib-adv_api_evaluation_2_end__
# __rllib-adv_api_evaluation_3_begin__
# Every time we run an evaluation step, run it for exactly 10 episodes, no matter,
# how many eval workers we have.
config = AlgorithmConfig().evaluation(
evaluation_duration=10,
evaluation_duration_unit="episodes",
# What if number of eval workers is non-dividable by 10?
# -> Run 7 episodes (1 per eval worker), then run 3 more episodes only using
# evaluation workers 1-3 (evaluation workers 4-7 remain idle during that time).
evaluation_num_env_runners=7,
)
# __rllib-adv_api_evaluation_3_end__
# __rllib-adv_api_evaluation_4_begin__
# Run evaluation and training at the same time via threading and make sure they roughly
# take the same time, such that the next `Algorithm.train()` call can execute
# immediately and not have to wait for a still ongoing (e.g. b/c of very long episodes)
# evaluation step:
config = AlgorithmConfig().evaluation(
evaluation_interval=2,
# run evaluation and training in parallel
evaluation_parallel_to_training=True,
# automatically end evaluation when train step has finished
evaluation_duration="auto",
evaluation_duration_unit="timesteps", # <- this setting is ignored; RLlib
# will always run by timesteps (not by complete
# episodes) in this duration=auto mode
)
# __rllib-adv_api_evaluation_4_end__
# __rllib-adv_api_evaluation_5_begin__
# Switching off exploration behavior for evaluation workers
# (see rllib/algorithms/algorithm.py). Use any keys in this sub-dict that are
# also supported in the main Algorithm config.
config = AlgorithmConfig().evaluation(
evaluation_config=AlgorithmConfig.overrides(explore=False),
)
# ... which is a more type-checked version of the old-style:
# config = AlgorithmConfig().evaluation(
# evaluation_config={"explore": False},
# )
# __rllib-adv_api_evaluation_5_end__
# __rllib-adv_api_evaluation_6_begin__
# Having an environment that occasionally blocks completely for e.g. 10min would
# also affect (and block) training. Here is how you can defend your evaluation setup
# against oft-crashing or -stalling envs (or other unstable components on your evaluation
# workers).
config = AlgorithmConfig().evaluation(
evaluation_interval=1,
evaluation_parallel_to_training=True,
evaluation_duration="auto",
evaluation_duration_unit="timesteps", # <- default anyway
evaluation_force_reset_envs_before_iteration=True, # <- default anyway
)
# __rllib-adv_api_evaluation_6_end__
@@ -0,0 +1,40 @@
# __rllib-custom-gym-env-begin__
import gymnasium as gym
import numpy as np
import ray
from ray.rllib.algorithms.ppo import PPOConfig
class SimpleCorridor(gym.Env):
def __init__(self, config):
self.end_pos = config["corridor_length"]
self.cur_pos = 0.0
self.action_space = gym.spaces.Discrete(2) # right/left
self.observation_space = gym.spaces.Box(0.0, self.end_pos, shape=(1,))
def reset(self, *, seed=None, options=None):
self.cur_pos = 0.0
return np.array([self.cur_pos]), {}
def step(self, action):
if action == 0 and self.cur_pos > 0.0: # move right (towards goal)
self.cur_pos -= 1.0
elif action == 1: # move left (towards start)
self.cur_pos += 1.0
if self.cur_pos >= self.end_pos:
return np.array([0.0]), 1.0, True, True, {}
else:
return np.array([self.cur_pos]), -0.1, False, False, {}
ray.init()
config = PPOConfig().environment(SimpleCorridor, env_config={"corridor_length": 5})
algo = config.build()
for _ in range(3):
print(algo.train())
algo.stop()
# __rllib-custom-gym-env-end__
@@ -0,0 +1,67 @@
import gymnasium as gym
import numpy as np
import tree # pip install dm_tree
from ray.rllib.algorithms.dreamerv3.dreamerv3 import DreamerV3Config
from ray.rllib.core.columns import Columns
from ray.rllib.utils.framework import convert_to_tensor
env_name = "CartPole-v1"
# Use the vector env API.
env = gym.make_vec(env_name, num_envs=1, vectorization_mode="sync")
terminated = truncated = False
# Reset the env.
obs, _ = env.reset()
# Every time, we start a new episode, we should set is_first to True for the upcoming
# action inference.
is_first = 1.0
# Create the algorithm from a simple config.
config = (
DreamerV3Config()
.environment("CartPole-v1")
.training(model_size="XS", training_ratio=1024)
)
algo = config.build()
# Extract the actual RLModule from the local (Dreamer) EnvRunner.
rl_module = algo.env_runner.module
# Get initial states from RLModule (note that these are always B=1, so this matches
# our num_envs=1; if you are using a vector env >1, you would have to repeat the
# returned states `num_env` times to get the correct batch size):
states = rl_module.get_initial_state()
# Batch the states to B=1.
states = tree.map_structure(lambda s: s.unsqueeze(0), states)
while not terminated and not truncated:
# Use the RLModule for action computations directly.
# DreamerV3 expects this particular batch format:
# obs=[B, T, ...]
# prev. states=[B, ...]
# `is_first`=[B]
batch = {
# States is already batched (see above).
Columns.STATE_IN: states,
# `obs` is already batched (due to vector env), but needs time-rank.
Columns.OBS: convert_to_tensor(obs, framework="torch")[None],
# Set to True at beginning of episode.
"is_first": convert_to_tensor(is_first, "torch")[None],
}
outs = rl_module.forward_inference(batch)
# Alternatively, call `forward_exploration` in case you want stochastic, non-greedy
# actions.
# outs = rl_module.forward_exploration(batch)
# Extract actions (remove time-rank) from outs.
actions = outs[Columns.ACTIONS].numpy()[0]
# Extract states from out. States are returned as batched.
states = outs[Columns.STATE_OUT]
# Perform a step in the env. Note that actions are still batched, which
# is ok, because we have a vector env.
obs, reward, terminated, truncated, info = env.step(actions)
# Not at the beginning of the episode anymore.
is_first = 0.0
@@ -0,0 +1,175 @@
# Demonstration of RLlib's ReplayBuffer workflow
from typing import Optional
import random
import numpy as np
from ray import tune
from ray.rllib.utils.replay_buffers import ReplayBuffer, StorageUnit
from ray.rllib.utils.annotations import override
from ray.rllib.utils.typing import SampleBatchType
from ray.rllib.utils.replay_buffers.utils import validate_buffer_config
from ray.rllib.examples.envs.classes.random_env import RandomEnv
from ray.rllib.policy.sample_batch import SampleBatch, concat_samples
from ray.rllib.algorithms.dqn.dqn import DQNConfig
# __sphinx_doc_replay_buffer_type_specification__begin__
config = (
DQNConfig()
.api_stack(
enable_env_runner_and_connector_v2=False, enable_rl_module_and_learner=False
)
.training(replay_buffer_config={"type": ReplayBuffer})
)
another_config = (
DQNConfig()
.api_stack(
enable_env_runner_and_connector_v2=False, enable_rl_module_and_learner=False
)
.training(replay_buffer_config={"type": "ReplayBuffer"})
)
yet_another_config = (
DQNConfig()
.api_stack(
enable_env_runner_and_connector_v2=False, enable_rl_module_and_learner=False
)
.training(
replay_buffer_config={"type": "ray.rllib.utils.replay_buffers.ReplayBuffer"}
)
)
validate_buffer_config(config)
validate_buffer_config(another_config)
validate_buffer_config(yet_another_config)
# After validation, all three configs yield the same effective config
assert (
config.replay_buffer_config
== another_config.replay_buffer_config
== yet_another_config.replay_buffer_config
)
# __sphinx_doc_replay_buffer_type_specification__end__
# __sphinx_doc_replay_buffer_basic_interaction__begin__
# We choose fragments because it does not impose restrictions on our batch to be added
buffer = ReplayBuffer(capacity=2, storage_unit=StorageUnit.FRAGMENTS)
dummy_batch = SampleBatch({"a": [1], "b": [2]})
buffer.add(dummy_batch)
buffer.sample(2)
# Because elements can be sampled multiple times, we receive a concatenated version
# of dummy_batch `{a: [1, 1], b: [2, 2,]}`.
# __sphinx_doc_replay_buffer_basic_interaction__end__
# __sphinx_doc_replay_buffer_own_buffer__begin__
class LessSampledReplayBuffer(ReplayBuffer):
@override(ReplayBuffer)
def sample(
self, num_items: int, evict_sampled_more_then: int = 30, **kwargs
) -> Optional[SampleBatchType]:
"""Evicts experiences that have been sampled > evict_sampled_more_then times."""
idxes = [random.randint(0, len(self) - 1) for _ in range(num_items)]
often_sampled_idxes = list(
filter(lambda x: self._hit_count[x] >= evict_sampled_more_then, set(idxes))
)
sample = self._encode_sample(idxes)
self._num_timesteps_sampled += sample.count
for idx in often_sampled_idxes:
del self._storage[idx]
self._hit_count = np.append(
self._hit_count[:idx], self._hit_count[idx + 1 :]
)
return sample
config = (
DQNConfig()
.api_stack(
enable_env_runner_and_connector_v2=False, enable_rl_module_and_learner=False
)
.environment(env="CartPole-v1")
.training(replay_buffer_config={"type": LessSampledReplayBuffer})
)
tune.Tuner(
"DQN",
param_space=config,
run_config=tune.RunConfig(
stop={"training_iteration": 1},
),
).fit()
# __sphinx_doc_replay_buffer_own_buffer__end__
# __sphinx_doc_replay_buffer_advanced_usage_storage_unit__begin__
# This line will make our buffer store only complete episodes found in a batch
config.training(replay_buffer_config={"storage_unit": StorageUnit.EPISODES})
less_sampled_buffer = LessSampledReplayBuffer(**config.replay_buffer_config)
# Gather some random experiences
env = RandomEnv()
terminated = truncated = False
batch = SampleBatch({})
t = 0
while not terminated and not truncated:
obs, reward, terminated, truncated, info = env.step([0, 0])
# Note that in order for RLlib to find out about start and end of an episode,
# "t" and "terminateds" have to properly mark an episode's trajectory
one_step_batch = SampleBatch(
{
"obs": [obs],
"t": [t],
"reward": [reward],
"terminateds": [terminated],
"truncateds": [truncated],
}
)
batch = concat_samples([batch, one_step_batch])
t += 1
less_sampled_buffer.add(batch)
for i in range(10):
assert len(less_sampled_buffer._storage) == 1
less_sampled_buffer.sample(num_items=1, evict_sampled_more_then=9)
assert len(less_sampled_buffer._storage) == 0
# __sphinx_doc_replay_buffer_advanced_usage_storage_unit__end__
# __sphinx_doc_replay_buffer_advanced_usage_underlying_buffers__begin__
config = (
DQNConfig()
.api_stack(
enable_env_runner_and_connector_v2=False, enable_rl_module_and_learner=False
)
.training(
replay_buffer_config={
"type": "MultiAgentReplayBuffer",
"underlying_replay_buffer_config": {
"type": LessSampledReplayBuffer,
# We can specify the default call argument
# for the sample method of the underlying buffer method here.
"evict_sampled_more_then": 20,
},
}
)
.environment(env="CartPole-v1")
)
tune.Tuner(
"DQN",
param_space=config.to_dict(),
run_config=tune.RunConfig(
stop={"env_runners/episode_return_mean": 40, "training_iteration": 7},
),
).fit()
# __sphinx_doc_replay_buffer_advanced_usage_underlying_buffers__end__
@@ -0,0 +1,170 @@
# __quick_start_begin__
import gymnasium as gym
import numpy as np
import torch
from typing import Dict, Tuple, Any, Optional
from ray.rllib.algorithms.ppo import PPOConfig
# Define your problem using python and Farama-Foundation's gymnasium API:
class SimpleCorridor(gym.Env):
"""Corridor environment where an agent must learn to move right to reach the exit.
---------------------
| S | 1 | 2 | 3 | G | S=start; G=goal; corridor_length=5
---------------------
Actions:
0: Move left
1: Move right
Observations:
A single float representing the agent's current position (index)
starting at 0.0 and ending at corridor_length
Rewards:
-0.1 for each step
+1.0 when reaching the goal
Episode termination:
When the agent reaches the goal (position >= corridor_length)
"""
def __init__(self, config):
self.end_pos = config["corridor_length"]
self.cur_pos = 0.0
self.action_space = gym.spaces.Discrete(2) # 0=left, 1=right
self.observation_space = gym.spaces.Box(0.0, self.end_pos, (1,), np.float32)
def reset(
self, *, seed: Optional[int] = None, options: Optional[Dict] = None
) -> Tuple[np.ndarray, Dict]:
"""Reset the environment for a new episode.
Args:
seed: Random seed for reproducibility
options: Additional options (not used in this environment)
Returns:
Initial observation of the new episode and an info dict.
"""
super().reset(seed=seed) # Initialize RNG if seed is provided
self.cur_pos = 0.0
# Return initial observation.
return np.array([self.cur_pos], np.float32), {}
def step(self, action: int) -> Tuple[np.ndarray, float, bool, bool, Dict]:
"""Take a single step in the environment based on the provided action.
Args:
action: 0 for left, 1 for right
Returns:
A tuple of (observation, reward, terminated, truncated, info):
observation: Agent's new position
reward: Reward from taking the action (-0.1 or +1.0)
terminated: Whether episode is done (reached goal)
truncated: Whether episode was truncated (always False here)
info: Additional information (empty dict)
"""
# Walk left if action is 0 and we're not at the leftmost position
if action == 0 and self.cur_pos > 0:
self.cur_pos -= 1
# Walk right if action is 1
elif action == 1:
self.cur_pos += 1
# Set `terminated` flag when end of corridor (goal) reached.
terminated = self.cur_pos >= self.end_pos
truncated = False
# +1 when goal reached, otherwise -0.1.
reward = 1.0 if terminated else -0.1
return np.array([self.cur_pos], np.float32), reward, terminated, truncated, {}
# Create an RLlib Algorithm instance from a PPOConfig object.
print("Setting up the PPO configuration...")
config = (
PPOConfig().environment(
# Env class to use (our custom gymnasium environment).
SimpleCorridor,
# Config dict passed to our custom env's constructor.
# Use corridor with 20 fields (including start and goal).
env_config={"corridor_length": 20},
)
# Parallelize environment rollouts for faster training.
.env_runners(num_env_runners=3)
# Use a smaller network for this simple task
.training(model={"fcnet_hiddens": [64, 64]})
)
# Construct the actual PPO algorithm object from the config.
algo = config.build_algo()
rl_module = algo.get_module()
# Train for n iterations and report results (mean episode rewards).
# Optimal reward calculation:
# - Need at least 19 steps to reach the goal (from position 0 to 19)
# - Each step (except last) gets -0.1 reward: 18 * (-0.1) = -1.8
# - Final step gets +1.0 reward
# - Total optimal reward: -1.8 + 1.0 = -0.8
print("\nStarting training loop...")
for i in range(5):
results = algo.train()
# Log the metrics from training results
print(f"Iteration {i+1}")
print(f" Training metrics: {results['env_runners']}")
# Save the trained algorithm (optional)
checkpoint_dir = algo.save()
print(f"\nSaved model checkpoint to: {checkpoint_dir}")
print("\nRunning inference with the trained policy...")
# Create a test environment with a shorter corridor to verify the agent's behavior
env = SimpleCorridor({"corridor_length": 10})
# Get the initial observation (should be: [0.0] for the starting position).
obs, info = env.reset()
terminated = truncated = False
total_reward = 0.0
step_count = 0
# Play one episode and track the agent's trajectory
print("\nAgent trajectory:")
positions = [float(obs[0])] # Track positions for visualization
while not terminated and not truncated and step_count < 1000:
# Compute an action given the current observation
action_logits = rl_module.forward_inference(
{"obs": torch.from_numpy(obs).unsqueeze(0)}
)["action_dist_inputs"].numpy()[
0
] # [0]: Batch dimension=1
# Get the action with highest probability
action = np.argmax(action_logits)
# Log the agent's decision
action_name = "LEFT" if action == 0 else "RIGHT"
print(f" Step {step_count}: Position {obs[0]:.1f}, Action: {action_name}")
# Apply the computed action in the environment
obs, reward, terminated, truncated, info = env.step(action)
positions.append(float(obs[0]))
# Sum up rewards
total_reward += reward
step_count += 1
# Report final results
print(f"\nEpisode complete:")
print(f" Steps taken: {step_count}")
print(f" Total reward: {total_reward:.2f}")
print(f" Final position: {obs[0]:.1f}")
# Verify the agent has learned the optimal policy
if total_reward > -0.5 and obs[0] >= 9.0:
print(" Success! The agent has learned the optimal policy (always move right).")
else:
print(" Failure! The agent didn't reach the goal within 1000 timesteps.")
# __quick_start_end__
+270
View File
@@ -0,0 +1,270 @@
# 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)