From 16e550c4c4f20217f8fb0f8f298442d0c6be56f5 Mon Sep 17 00:00:00 2001 From: will Date: Mon, 2 Jan 2023 17:34:05 +0000 Subject: [PATCH 1/7] added pr tests --- .../workflows/linux-tutorials-test-on-pr.yml | 32 +++++ ...t.yml => linux-tutorials-test-on-push.yml} | 0 tests/DatasetCreation/dataset_creation.py | 118 ++++++++++++++++++ tests/DatasetCreation/requirements.txt | 4 + tests/DatasetUsage/dataset_usage.py | 24 ++++ tests/DatasetUsage/requirements.txt | 4 + tests/LocalStorage/local_storage.py | 116 +++++++++++++++++ tests/LocalStorage/requirements.txt | 8 ++ 8 files changed, 306 insertions(+) create mode 100644 .github/workflows/linux-tutorials-test-on-pr.yml rename .github/workflows/{linux-tutorials-test.yml => linux-tutorials-test-on-push.yml} (100%) create mode 100644 tests/DatasetCreation/dataset_creation.py create mode 100644 tests/DatasetCreation/requirements.txt create mode 100644 tests/DatasetUsage/dataset_usage.py create mode 100644 tests/DatasetUsage/requirements.txt create mode 100644 tests/LocalStorage/local_storage.py create mode 100644 tests/LocalStorage/requirements.txt diff --git a/.github/workflows/linux-tutorials-test-on-pr.yml b/.github/workflows/linux-tutorials-test-on-pr.yml new file mode 100644 index 00000000..df6a47a3 --- /dev/null +++ b/.github/workflows/linux-tutorials-test-on-pr.yml @@ -0,0 +1,32 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions +--- +name: PR tests + +on: + pull_request: + +permissions: + contents: read + +jobs: + tutorial-test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10'] + tutorial: ['LocalStorage', 'DatasetUsage', 'DatasetCreation'] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies and run tutorials + run: | + cd tests/${{ matrix.tutorial }} + pip install -r requirements.txt + pip uninstall -y minari + pip install -e ../.. + for f in *.py; do python "$f"; done diff --git a/.github/workflows/linux-tutorials-test.yml b/.github/workflows/linux-tutorials-test-on-push.yml similarity index 100% rename from .github/workflows/linux-tutorials-test.yml rename to .github/workflows/linux-tutorials-test-on-push.yml diff --git a/tests/DatasetCreation/dataset_creation.py b/tests/DatasetCreation/dataset_creation.py new file mode 100644 index 00000000..adf46918 --- /dev/null +++ b/tests/DatasetCreation/dataset_creation.py @@ -0,0 +1,118 @@ +# pyright: basic, reportOptionalMemberAccess=false + +import base64 +import json +import os + +import gymnasium as gym +import numpy as np +from gymnasium.utils.serialize_spec_stack import serialise_spec_stack + +import minari +from minari.dataset import MinariDataset + +# 1. Get permissions to upload to GCP +# This test is called on PR, so we skip the GCP part - the GCP part is tested only on merge to main due to security reasons. + +# 2. Standard Gymnasium procedure to collect data into whatever replay buffer you want +env = gym.make("FetchReach-v3") + +environment_stack = serialise_spec_stack( + env.spec_stack +) # Get the environment specification stack for reproducibility + +env.reset() +replay_buffer = { + "episode": np.array([]), + "observation": np.array([]), + "action": np.array([]), + "reward": np.array([]), + "terminated": np.array([]), + "truncated": np.array([]), +} +dataset_name = "FetchReach_v3_example-dataset" + +num_episodes = 4 + + +assert env.spec.max_episode_steps is not None, "Max episode steps must be defined" + +replay_buffer = { + "episode": np.array( + [[0]] * env.spec.max_episode_steps * num_episodes, dtype=np.int32 + ), + "observation": np.array( + [[0] * 13] * env.spec.max_episode_steps * num_episodes, + dtype=np.float32, + ), + "action": np.array( + [[0] * 4] * env.spec.max_episode_steps * num_episodes, dtype=np.float32 + ), + "reward": np.array( + [[0]] * env.spec.max_episode_steps * num_episodes, dtype=np.float32 + ), + "terminated": np.array( + [[0]] * env.spec.max_episode_steps * num_episodes, dtype=bool + ), + "truncated": np.array( + [[0]] * env.spec.max_episode_steps * num_episodes, dtype=bool + ), +} + +total_steps = 0 +for episode in range(num_episodes): + episode_step = 0 + observation, info = env.reset() + terminated = False + truncated = False + while not terminated and not truncated: + action = env.action_space.sample() # User-defined policy function + observation, reward, terminated, truncated, info = env.step(action) + replay_buffer["episode"][total_steps] = np.array(episode) + replay_buffer["observation"][total_steps] = np.concatenate( + ( + np.array(observation["observation"]), + np.array(observation["desired_goal"]), + ) + ) + replay_buffer["action"][total_steps] = np.array(action) + replay_buffer["reward"][total_steps] = np.array(reward) + replay_buffer["terminated"][total_steps] = np.array(terminated) + replay_buffer["truncated"][total_steps] = np.array(truncated) + episode_step += 1 + total_steps += 1 + +env.close() + +replay_buffer["episode"] = replay_buffer["episode"][:total_steps] +replay_buffer["observation"] = replay_buffer["observation"][:total_steps] +replay_buffer["action"] = replay_buffer["action"][:total_steps] +replay_buffer["reward"] = replay_buffer["reward"][:total_steps] +replay_buffer["terminated"] = replay_buffer["terminated"][:total_steps] +replay_buffer["truncated"] = replay_buffer["truncated"][:total_steps] + + +# 3. Convert the replay buffer to a MinariDataset +dataset = MinariDataset( + dataset_name=dataset_name, + algorithm_name="random_policy", + environment_name="FetchReach-v3", + environment_stack=json.dumps(environment_stack), + seed_used=42, # For the simplicity of this example, we're not actually using a seed. Naughty us! + code_permalink="https://github.com/Farama-Foundation/Minari/blob/f095bfe07f8dc6642082599e07779ec1dd9b2667/tutorials/LocalStorage/local_storage.py", + author="WillDudley", + author_email="wdudley@farama.org", + observations=replay_buffer["observation"], + actions=replay_buffer["action"], + rewards=replay_buffer["reward"], + terminations=replay_buffer["terminated"], + truncations=replay_buffer["truncated"], +) + +print("Dataset generated!") + +# 4. Save the dataset locally +dataset.save() + +# 5. Upload the dataset to GCP +# This test is called on PR, so we skip the GCP part - the GCP part is tested only on merge to main due to security reasons. diff --git a/tests/DatasetCreation/requirements.txt b/tests/DatasetCreation/requirements.txt new file mode 100644 index 00000000..80e1f32c --- /dev/null +++ b/tests/DatasetCreation/requirements.txt @@ -0,0 +1,4 @@ +gymnasium-robotics +cython +#minari +git+https://github.com/WillDudley/Gymnasium.git@spec_stack#egg=gymnasium \ No newline at end of file diff --git a/tests/DatasetUsage/dataset_usage.py b/tests/DatasetUsage/dataset_usage.py new file mode 100644 index 00000000..88fd6e2d --- /dev/null +++ b/tests/DatasetUsage/dataset_usage.py @@ -0,0 +1,24 @@ +import base64 +import json +import os + +import gymnasium as gym +from gymnasium.utils.serialize_spec_stack import deserialise_spec_stack + +import minari + +dataset = minari.download_dataset("LunarLander_v2_remote-test-dataset") + +print("*" * 60, " Dataset Structure") +print(f"Dataset attributes: {dataset.__dir__()}\n") +print(f"Episode attributes: {dataset.episodes[0].__dir__()}\n") +print(f"Transition attributes: {dataset.episodes[0].transitions[0].__dir__()}\n") + +print("*" * 60, " Examples") +print(f"Shape of observations: {dataset.observations.shape}\n") +print(f"Return of third episode: {dataset.episodes[2].compute_return()}\n") +print(f"21st action of fifth episode: {dataset.episodes[4].transitions[20].action}\n") + +reconstructed_environment = gym.make( + deserialise_spec_stack(json.loads(dataset.environment_stack)) +) diff --git a/tests/DatasetUsage/requirements.txt b/tests/DatasetUsage/requirements.txt new file mode 100644 index 00000000..ffdcbc9e --- /dev/null +++ b/tests/DatasetUsage/requirements.txt @@ -0,0 +1,4 @@ +numpy +cython +#minari +git+https://github.com/WillDudley/Gymnasium.git@spec_stack#egg=gymnasium[box2d] diff --git a/tests/LocalStorage/local_storage.py b/tests/LocalStorage/local_storage.py new file mode 100644 index 00000000..26761570 --- /dev/null +++ b/tests/LocalStorage/local_storage.py @@ -0,0 +1,116 @@ +# pyright: basic, reportOptionalMemberAccess=false, reportOptionalSubscript=false +import json + +import gymnasium as gym +import numpy as np +from gymnasium.utils.serialize_spec_stack import serialise_spec_stack + +import minari +from minari.dataset import MinariDataset + + +def generate_dataset(dataset_name: str): + num_episodes = 10 + + env = gym.make("LunarLander-v2", render_mode="rgb_array") + environment_stack = serialise_spec_stack(env.spec_stack) + observation, info = env.reset(seed=42) + + assert env.spec.max_episode_steps is not None, "Max episode steps must be defined" + + replay_buffer = { + "episode": np.array( + [[0]] * env.spec.max_episode_steps * num_episodes, dtype=np.int32 + ), + "observation": np.array( + [[0] * env.observation_space.shape[0]] + * env.spec.max_episode_steps + * num_episodes, + dtype=np.float32, + ), + "action": np.array( + [[0] * 2] * env.spec.max_episode_steps * num_episodes, dtype=np.float32 + ), + "reward": np.array( + [[0]] * env.spec.max_episode_steps * num_episodes, dtype=np.float32 + ), + "terminated": np.array( + [[0]] * env.spec.max_episode_steps * num_episodes, dtype=bool + ), + "truncated": np.array( + [[0]] * env.spec.max_episode_steps * num_episodes, dtype=bool + ), + } + + total_steps = 0 + for episode in range(num_episodes): + episode_step = 0 + observation, info = env.reset() + terminated = False + truncated = False + while not terminated and not truncated: + action = env.action_space.sample() # User-defined policy function + observation, reward, terminated, truncated, info = env.step(action) + replay_buffer["episode"][total_steps] = np.array(episode) + replay_buffer["observation"][total_steps] = np.array(observation) + replay_buffer["action"][total_steps] = np.array(action) + replay_buffer["reward"][total_steps] = np.array(reward) + replay_buffer["terminated"][total_steps] = np.array(terminated) + replay_buffer["truncated"][total_steps] = np.array(truncated) + episode_step += 1 + total_steps += 1 + + env.close() + + replay_buffer["episode"] = replay_buffer["episode"][:total_steps] + replay_buffer["observation"] = replay_buffer["observation"][:total_steps] + replay_buffer["action"] = replay_buffer["action"][:total_steps] + replay_buffer["reward"] = replay_buffer["reward"][:total_steps] + replay_buffer["terminated"] = replay_buffer["terminated"][:total_steps] + replay_buffer["truncated"] = replay_buffer["truncated"][:total_steps] + + ds = MinariDataset( + dataset_name=dataset_name, + algorithm_name="random_policy", + environment_name="LunarLander-v2", + environment_stack=json.dumps(environment_stack), + seed_used=42, # For the simplicity of this example, we're not actually using a seed. Naughty us! + code_permalink="https://github.com/Farama-Foundation/Minari/blob/f095bfe07f8dc6642082599e07779ec1dd9b2667/tutorials/LocalStorage/local_storage.py", + author="WillDudley", + author_email="wdudley@farama.org", + observations=replay_buffer["observation"], + actions=replay_buffer["action"], + rewards=replay_buffer["reward"], + terminations=replay_buffer["terminated"], + truncations=replay_buffer["truncated"], + ) + + print("Dataset generated!") + + return ds + + +if __name__ == "__main__": + dataset_name = "LunarLander_v2_test-dataset" + + print("\n Generate dataset as standard") + generated_dataset = generate_dataset(dataset_name) + + print("\n Save dataset to local storage") + generated_dataset.save() + + print( + "\n Listing datasets in local storage, we should see the dataset we just generated" + ) + minari.list_local_datasets() + + print("\n We can load the dataset from local storage as follows") + loaded_dataset = minari.load_dataset(dataset_name) + + print("\n We can delete the dataset from local storage as follows") + minari.delete_dataset(dataset_name) + + print( + "\n Listing datasets in local storage, we should now no longer see the dataset we just generated" + ) + minari.list_local_datasets() diff --git a/tests/LocalStorage/requirements.txt b/tests/LocalStorage/requirements.txt new file mode 100644 index 00000000..b716a315 --- /dev/null +++ b/tests/LocalStorage/requirements.txt @@ -0,0 +1,8 @@ +#gymnasium[box2d] # todo: vc +cython +#minari +h5py +structlog +tensorboardX +typing_extensions +git+https://github.com/WillDudley/Gymnasium.git@spec_stack#egg=gymnasium[box2d] \ No newline at end of file From 97ba4f28f72469a6bfcc2f31e9574c05bc50873f Mon Sep 17 00:00:00 2001 From: will Date: Mon, 2 Jan 2023 17:45:40 +0000 Subject: [PATCH 2/7] precommit --- tests/DatasetCreation/dataset_creation.py | 3 --- tests/DatasetUsage/dataset_usage.py | 4 ++-- tutorials/DatasetUsage/dataset_usage.py | 2 ++ 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/DatasetCreation/dataset_creation.py b/tests/DatasetCreation/dataset_creation.py index adf46918..7cd65487 100644 --- a/tests/DatasetCreation/dataset_creation.py +++ b/tests/DatasetCreation/dataset_creation.py @@ -1,14 +1,11 @@ # pyright: basic, reportOptionalMemberAccess=false -import base64 import json -import os import gymnasium as gym import numpy as np from gymnasium.utils.serialize_spec_stack import serialise_spec_stack -import minari from minari.dataset import MinariDataset # 1. Get permissions to upload to GCP diff --git a/tests/DatasetUsage/dataset_usage.py b/tests/DatasetUsage/dataset_usage.py index 88fd6e2d..c9920249 100644 --- a/tests/DatasetUsage/dataset_usage.py +++ b/tests/DatasetUsage/dataset_usage.py @@ -1,6 +1,6 @@ -import base64 +# pyright: basic, reportGeneralTypeIssues=false + import json -import os import gymnasium as gym from gymnasium.utils.serialize_spec_stack import deserialise_spec_stack diff --git a/tutorials/DatasetUsage/dataset_usage.py b/tutorials/DatasetUsage/dataset_usage.py index 4b7333fc..7c0a403f 100644 --- a/tutorials/DatasetUsage/dataset_usage.py +++ b/tutorials/DatasetUsage/dataset_usage.py @@ -1,3 +1,5 @@ +# pyright: basic, reportGeneralTypeIssues=false + import base64 import json import os From 01a31d666ac500cd9ab36207629a42243a78c22a Mon Sep 17 00:00:00 2001 From: will Date: Tue, 3 Jan 2023 11:17:58 +0000 Subject: [PATCH 3/7] change download procedure --- minari/storage/hosting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minari/storage/hosting.py b/minari/storage/hosting.py index f505c386..04fa387a 100644 --- a/minari/storage/hosting.py +++ b/minari/storage/hosting.py @@ -36,7 +36,7 @@ def download_dataset(dataset_name: str): ) project_id = "dogwood-envoy-367012" bucket_name = "minari" - storage_client = storage.Client(project=project_id) + storage_client = storage.Client.create_anonymous_client() bucket = storage_client.bucket(bucket_name) From a4a76eb4748dfb9755059b6fc9fb4e2b258d7e07 Mon Sep 17 00:00:00 2001 From: will Date: Tue, 3 Jan 2023 11:20:30 +0000 Subject: [PATCH 4/7] change list remote procedure --- minari/storage/hosting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minari/storage/hosting.py b/minari/storage/hosting.py index 04fa387a..a713a060 100644 --- a/minari/storage/hosting.py +++ b/minari/storage/hosting.py @@ -57,7 +57,7 @@ def download_dataset(dataset_name: str): def list_remote_datasets(): project_id = "dogwood-envoy-367012" bucket_name = "minari" - storage_client = storage.Client(project=project_id) + storage_client = storage.Client.create_anonymous_client() bucket = storage_client.bucket(bucket_name) blobs = bucket.list_blobs() From 0112b132898e56d061d458924fdca2f72ff479f0 Mon Sep 17 00:00:00 2001 From: will Date: Tue, 3 Jan 2023 11:20:56 +0000 Subject: [PATCH 5/7] refactor --- minari/storage/hosting.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/minari/storage/hosting.py b/minari/storage/hosting.py index a713a060..10cc12de 100644 --- a/minari/storage/hosting.py +++ b/minari/storage/hosting.py @@ -34,7 +34,6 @@ def download_dataset(dataset_name: str): print( f"Dataset not found locally. Downloading {dataset_name} from Farama servers..." ) - project_id = "dogwood-envoy-367012" bucket_name = "minari" storage_client = storage.Client.create_anonymous_client() @@ -55,7 +54,6 @@ def download_dataset(dataset_name: str): def list_remote_datasets(): - project_id = "dogwood-envoy-367012" bucket_name = "minari" storage_client = storage.Client.create_anonymous_client() From c133b5e4e3f14c768d3e423ac32d39142b958cd2 Mon Sep 17 00:00:00 2001 From: will Date: Tue, 3 Jan 2023 11:24:51 +0000 Subject: [PATCH 6/7] remove unneeded creds --- tutorials/DatasetUsage/dataset_usage.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tutorials/DatasetUsage/dataset_usage.py b/tutorials/DatasetUsage/dataset_usage.py index 7c0a403f..61e601f6 100644 --- a/tutorials/DatasetUsage/dataset_usage.py +++ b/tutorials/DatasetUsage/dataset_usage.py @@ -9,14 +9,6 @@ import minari -GCP_DATASET_ADMIN = os.environ["GCP_DATASET_ADMIN"] - -credentials_json = base64.b64decode(GCP_DATASET_ADMIN).decode("utf8").replace("'", '"') -with open("credentials.json", "w") as f: - f.write(credentials_json) - -os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "./credentials.json" - dataset = minari.download_dataset("LunarLander_v2_remote-test-dataset") print("*" * 60, " Dataset Structure") From 46a88377eacded7c0ab6627475d4ae1f2fddeb6d Mon Sep 17 00:00:00 2001 From: will Date: Tue, 3 Jan 2023 11:25:39 +0000 Subject: [PATCH 7/7] precommit --- tutorials/DatasetUsage/dataset_usage.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tutorials/DatasetUsage/dataset_usage.py b/tutorials/DatasetUsage/dataset_usage.py index 61e601f6..c9920249 100644 --- a/tutorials/DatasetUsage/dataset_usage.py +++ b/tutorials/DatasetUsage/dataset_usage.py @@ -1,8 +1,6 @@ # pyright: basic, reportGeneralTypeIssues=false -import base64 import json -import os import gymnasium as gym from gymnasium.utils.serialize_spec_stack import deserialise_spec_stack