Files
2026-07-13 13:17:40 +08:00

240 lines
8.9 KiB
Python

import pathlib
import shutil
import unittest
import msgpack
import msgpack_numpy as m
import ray
from ray.rllib.algorithms.ppo.ppo import PPOConfig
from ray.rllib.core.columns import Columns
from ray.rllib.core.rl_module.default_model_config import DefaultModelConfig
from ray.rllib.env.single_agent_episode import SingleAgentEpisode
from ray.rllib.offline.offline_data import OfflineData
from ray.rllib.offline.offline_env_runner import OfflineSingleAgentEnvRunner
class TestOfflineEnvRunner(unittest.TestCase):
def setUp(self) -> None:
self.base_path = pathlib.Path("/tmp/")
self.config = (
PPOConfig()
.env_runners(
# This defines how many rows per file we will
# have (given `num_rows_per_file` in the
# `output_write_method_kwargs` is not set).
rollout_fragment_length=1000,
num_env_runners=0,
# Note, this means that written episodes. if
# `output_write_episodes=True` will be incomplete
# in many cases.
batch_mode="truncate_episodes",
)
.environment("CartPole-v1")
.rl_module(
# Use a small network for this test.
model_config=DefaultModelConfig(
fcnet_hiddens=[32],
fcnet_activation="linear",
vf_share_layers=True,
)
)
)
ray.init()
def tearDown(self) -> None:
ray.shutdown()
def test_offline_env_runner_record_episodes(self):
"""Tests recording of episodes.
Note, in this case each row of the dataset is an episode
that could potentially contain hundreds of steps.
"""
data_dir = pathlib.Path("local://") / self.base_path / "cartpole-episodes"
config = self.config.offline_data(
output=data_dir.as_posix(),
# Store experiences in episodes.
output_write_episodes=True,
)
offline_env_runner = OfflineSingleAgentEnvRunner(config=config, worker_index=1)
# Sample 100 episodes.
_ = offline_env_runner.sample(
num_episodes=100,
random_actions=True,
)
data_path = data_dir / self._get_dir_name(offline_env_runner)
records = list(data_path.iterdir())
self.assertEqual(len(records), 1)
self.assertEqual(records[0].name, "run-000001-00001")
# Now read in episodes.
config = self.config.offline_data(
input_=[data_path.as_posix()],
input_read_episodes=True,
)
offline_data = OfflineData(config)
# Assert the dataset has only 100 rows (each row containing an episode).
self.assertEqual(offline_data.data.count(), 100)
# Take a single row and ensure its a `SingleAgentEpisode` instance.
self.assertIsInstance(
SingleAgentEpisode.from_state(
msgpack.unpackb(
offline_data.data.take(1)[0]["item"], object_hook=m.decode
)
),
SingleAgentEpisode,
)
# The batch contains now episodes (in a numpy.NDArray).
episodes = offline_data.data.take_batch(100)["item"]
# The batch should contain 100 episodes (not 100 env steps).
self.assertEqual(len(episodes), 100)
# Remove all data.
shutil.rmtree(data_dir)
def test_offline_env_runner_record_column_data(self):
"""Tests recording of single time steps in column format.
Note, in this case each row in the dataset contains only a single
timestep of the agent.
"""
data_dir = pathlib.Path("local://") / self.base_path / "cartpole-columns"
config = self.config.offline_data(
output=data_dir.as_posix(),
# Store experiences in episodes.
output_write_episodes=False,
# Do not compress columns.
output_compress_columns=[],
)
offline_env_runner = OfflineSingleAgentEnvRunner(config=config, worker_index=1)
_ = offline_env_runner.sample(
num_timesteps=100,
random_actions=True,
)
data_path = data_dir / self._get_dir_name(offline_env_runner)
records = list(data_path.iterdir())
self.assertEqual(len(records), 1)
self.assertEqual(records[0].name, "run-000001-00001")
# Now read in episodes.
config = self.config.offline_data(
input_=[data_path.as_posix()],
input_read_episodes=False,
)
offline_data = OfflineData(config)
# Assert the dataset has only 100 rows.
self.assertEqual(offline_data.data.count(), 100)
# The batch contains now episodes (in a numpy.NDArray).
batch = offline_data.data.take_batch(100)
# The batch should contain 100 episodes (not 100 env steps).
self.assertTrue(len(batch[Columns.OBS]) == 100)
# Remove all data.
shutil.rmtree(data_dir)
def test_offline_env_runner_compress_columns(self):
"""Tests recording of timesteps with compressed columns.
Note, `input_compress_columns` will compress only the columns
listed. `Columns.OBS` will also compress `Columns.NEXT_OBS`.
"""
data_dir = pathlib.Path("local://") / self.base_path / "cartpole-columns"
config = self.config.offline_data(
output=data_dir.as_posix(),
# Store experiences in episodes.
output_write_episodes=False,
# LZ4-compress columns 'obs', 'new_obs', and 'actions' to
# save disk space and increase performance. Note, this means
# that you have to use `input_compress_columns` in the same
# way when using the data for training in `RLlib`.
output_compress_columns=[Columns.OBS, Columns.ACTIONS],
# In addition compress the complete file.
# TODO (simon): This does not work. It looks as if there
# is an error in the write/read methods for qparquet in
# ray.data. `arrow_open_stream_args` nor `arrow_parquet_args`
# do work here.
# output_write_method_kwargs={
# "arrow_open_stream_args": {
# "compression": "gzip",
# }
# }
)
offline_env_runner = OfflineSingleAgentEnvRunner(config=config, worker_index=1)
_ = offline_env_runner.sample(
num_timesteps=100,
random_actions=True,
)
data_path = data_dir / self._get_dir_name(offline_env_runner)
records = list(data_path.iterdir())
self.assertEqual(len(records), 1)
self.assertEqual(records[0].name, "run-000001-00001")
# Now read in episodes.
config = self.config.offline_data(
input_=[(data_path / "run-000001-00001").as_posix()],
input_read_episodes=False,
# Also uncompress files and columns.
# TODO (simon): Activate as soon as the bug is fixed
# in ray.data.
# input_read_method_kwargs={
# "arrow_open_stream_args": {
# "compression": "gzip",
# }
# },
input_compress_columns=[Columns.OBS, Columns.ACTIONS],
)
offline_data = OfflineData(config)
# Assert the dataset has only 100 rows.
self.assertEqual(offline_data.data.count(), 100)
# The batch contains now episodes (in a numpy.NDArray).
batch = offline_data.data.take_batch(100)
# The batch should contain 100 episodes (not 100 env steps).
self.assertTrue(len(batch[Columns.OBS]) == 100)
# Remove all data.
shutil.rmtree(data_dir)
@staticmethod
def _get_dir_name(offline_env_runner):
if offline_env_runner.env:
# Set the subdir (environment specific).
if isinstance(offline_env_runner.env, str):
# `env` is a string.
offline_env_runner.subdir_path = offline_env_runner.env.lower()
else:
# `env`` is a class or callable we use its class name.
offline_env_runner.subdir_path = offline_env_runner.env.unwrapped.envs[
0
].unwrapped.__class__.__name__.lower()
return offline_env_runner.subdir_path
elif not offline_env_runner.env and (
(
offline_env_runner.config.create_env_on_local_worker
and offline_env_runner.worker_index == 0
)
or offline_env_runner.worker_index > 0
):
raise ValueError(
"To set up the output path, the environment "
"`env` must be provided when creating the "
"`OfflineSingleAgentEnvRunner`."
)
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))