From 9b10fbc6a9d9d2dc31bb01088e0333844856ebfc Mon Sep 17 00:00:00 2001 From: Tucker Alban Date: Tue, 1 Aug 2023 10:05:10 -0400 Subject: [PATCH] Update panda3d for python 3.10 (#2080) * Update panda3d for python 3.10 * Unpin cloudpickle. * Fix figure eight. * Attempt colab fixes. * Update setup to support python 3.10. * Attempt further fixes. * Fix tests. * Update dependencies and add version install test. * Fix checkout action. * Remove dev tags. * Fix wrapper numpy integer. * Run version test only on master. * Update changelog. --- .github/workflows/ci-python-version-test.yml | 30 ++++ CHANGELOG.md | 2 + examples/env/__init__.py | 0 examples/env/create_run_visualize.ipynb | 44 ++--- examples/env/crv.py | 31 ++++ examples/env/figure_eight_env.py | 10 +- examples/tests/test_examples.py | 2 +- setup.cfg | 22 +-- smarts/core/utils/episodes.py | 8 +- smarts/engine.ini | 2 +- smarts/env/gymnasium/hiway_env_v1.py | 5 +- smarts/env/gymnasium/wrappers/metric/utils.py | 2 +- smarts/env/wrappers/record_video.py | 168 ------------------ 13 files changed, 113 insertions(+), 213 deletions(-) create mode 100644 .github/workflows/ci-python-version-test.yml create mode 100644 examples/env/__init__.py create mode 100644 examples/env/crv.py delete mode 100644 smarts/env/wrappers/record_video.py diff --git a/.github/workflows/ci-python-version-test.yml b/.github/workflows/ci-python-version-test.yml new file mode 100644 index 0000000000..fce0710f5e --- /dev/null +++ b/.github/workflows/ci-python-version-test.yml @@ -0,0 +1,30 @@ +name: SMARTS CI Python Version Tests + +on: + push: + branches: + - master + +env: + venv_dir: .venv + +jobs: + install_python: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11'] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Update requirements + run: | + cd $GITHUB_WORKSPACE + python${{ matrix.python-version }} -m venv ${{env.venv_dir}} + . ${{env.venv_dir}}/bin/activate + pip install --upgrade pip + pip install wheel==0.38.4 + pip install .[camera_obs,rllib,sumo,test,torch,train] \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0bddef28..71aaac0a45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,10 @@ Copy and pasting the git commit messages is __NOT__ enough. ## [Unreleased] - XXXX-XX-XX ### Added ### Changed +- The following dependencies have been loosened: `numpy`, `opencv`, `torch`. ### Deprecated ### Fixed +- The `smarts` package now works with `python3.10` and `python3.11`. ### Removed ### Security diff --git a/examples/env/__init__.py b/examples/env/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/env/create_run_visualize.ipynb b/examples/env/create_run_visualize.ipynb index 99c5a18468..3e20cf74d3 100644 --- a/examples/env/create_run_visualize.ipynb +++ b/examples/env/create_run_visualize.ipynb @@ -1,6 +1,7 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "metadata": { "id": "wkR0YvENQni4" @@ -22,7 +23,7 @@ "outputs": [], "source": [ "!git clone https://github.com/huawei-noah/SMARTS 2> /dev/null\n", - "!cd SMARTS && ls && git checkout develop && pip install .[camera_obs]" + "!cd SMARTS && ls && git checkout tucker/fix_for_python_3_10 && pip install .[camera_obs,gymnasium]" ] }, { @@ -37,12 +38,16 @@ }, "outputs": [], "source": [ + "import os\n", "import sys\n", + "from pathlib import Path\n", "\n", - "sys.path.insert(0, \"/content/SMARTS/\")" + "sys.path.insert(0, Path(os.path.abspath(\"\")).parents[1])\n", + "print(Path(os.path.abspath(\"\")) / \"SMARTS\")" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "id": "s7UtcphinvNv" @@ -59,13 +64,14 @@ }, "outputs": [], "source": [ - "import gym\n", + "import gymnasium as gym\n", "\n", "from smarts.zoo import registry\n", "from smarts.env.gymnasium.wrappers.episode_logger import EpisodeLogger" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "id": "LFoG7Z-FobPP" @@ -88,14 +94,13 @@ "outputs": [], "source": [ "from smarts.core.utils.episodes import episode_range\n", - "from smarts.env.wrappers.record_video import RecordVideo\n", + "from smarts.core.utils.import_utils import import_module_from_file\n", "\n", - "import examples.env.figure_eight_env\n", - "\n", - "env = gym.make(\"figure_eight-v0\")\n", - "env: gym.Env = RecordVideo(\n", - " env, video_folder=\"videos\", video_length=40, step_trigger=lambda s: s % 100 == 0\n", + "import_module_from_file(\n", + " \"examples.env.figure_eight_env\", Path(os.path.abspath(\"\")) / \"figure_eight_env.py\"\n", ")\n", + "\n", + "env = gym.make(\"figure_eight-v0\", disable_env_checker=True)\n", "env: gym.Env = EpisodeLogger(env)\n", "\n", "import zoo.policies.keep_lane_agent\n", @@ -104,24 +109,13 @@ "\n", "for episode in episode_range(max_steps=450):\n", " observation = env.reset()\n", - " reward, done, info = None, False, None\n", - " while episode.continues(observation, reward, done, info):\n", + " reward, terminated, truncated, info = None, False, False, None\n", + " while episode.continues(observation, reward, terminated, truncated, info):\n", " action = agent.act(observation)\n", - " observation, reward, done, info = env.step(action)\n", + " observation, reward, terminated, info = env.step(action)\n", "\n", "env.close()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from smarts.env.wrappers.utils.rendering import show_notebook_videos\n", - "\n", - "show_notebook_videos()" - ] } ], "metadata": { @@ -130,7 +124,7 @@ "provenance": [] }, "kernelspec": { - "display_name": "Python 3.8.10 ('.venv': venv)", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -148,7 +142,7 @@ }, "vscode": { "interpreter": { - "hash": "94261e0756b8490ed1a668b85ed1da8c98261f1072b842b18d4e3da7517b644d" + "hash": "983a288b1deae516b5d1a3268f286490dc1a3bd215a042403a142fb3df5f8acd" } } }, diff --git a/examples/env/crv.py b/examples/env/crv.py new file mode 100644 index 0000000000..060bde1e92 --- /dev/null +++ b/examples/env/crv.py @@ -0,0 +1,31 @@ +import os +import sys +from pathlib import Path + +import gymnasium as gym +from gymnasium.wrappers.record_video import RecordVideo + +from smarts.core.utils.episodes import Episode, episode_range, episodes +from smarts.env.gymnasium.wrappers.episode_logger import EpisodeLogger +from smarts.zoo import registry + +SMARTS_DIR = Path(os.path.abspath("")) +sys.path.insert(0, SMARTS_DIR) + +from examples.env import figure_eight_env + +env = gym.make("figure_eight-v0", disable_env_checker=True) +env: gym.Env = EpisodeLogger(env) + +import zoo.policies.keep_lane_agent + +agent = registry.make_agent("zoo.policies:keep-lane-agent-v0") + +for episode in episode_range(max_steps=450): + observation, info = env.reset() + reward, terminated, truncated, info = None, False, False, None + while episode.continues(observation, reward, terminated, truncated, info): + action = agent.act(observation) + observation, reward, _, terminated, info = env.step(action) + +env.close() diff --git a/examples/env/figure_eight_env.py b/examples/env/figure_eight_env.py index e758677d88..f1878c6b45 100644 --- a/examples/env/figure_eight_env.py +++ b/examples/env/figure_eight_env.py @@ -4,6 +4,8 @@ from smarts.core.agent_interface import AgentInterface, AgentType from smarts.env.gymnasium.wrappers.single_agent import SingleAgent +from smarts.env.utils.action_conversion import ActionOptions +from smarts.env.utils.observation_conversion import ObservationOptions agent_interface = AgentInterface.from_type( AgentType.Laner, @@ -27,8 +29,14 @@ def entry_point(*args, **kwargs): agent_interfaces={"agent-007": agent_interface}, scenarios=[scenario], headless=True, + action_options=ActionOptions.unformatted, + observation_options=ObservationOptions.unformatted, + disable_env_checker=True, ) - env.metadata["render.modes"] = set(env.metadata["render.modes"]) | {"rgb_array"} + env.unwrapped.render_mode = "rgb_array" + env.metadata["render_modes"] = set(env.metadata.get("render_modes", ())) | { + "rgb_array" + } return SingleAgent(env) diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index bef11315f7..ec807db580 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -43,7 +43,7 @@ def test_rllib_pg_example(): from examples.rl.rllib import pg_example main = pg_example.main - with tempfile.TemporaryDirectory() as result_dir, tempfile.TemporaryDirectory() as model_dir: + with tempfile.TemporaryDirectory() as result_dir: main( scenarios=["./scenarios/sumo/loop"], envision=False, diff --git a/setup.cfg b/setup.cfg index 7251cb3499..07bfffc065 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,6 +11,7 @@ classifiers= Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 License :: OSI Approved :: MIT License [options] @@ -25,8 +26,7 @@ install_requires = setuptools>=41.0.0,!=50.0 click>=7.1.2 # used in scl # numpy>=1.19.5 required for tf 2.4 - # numpy<1.24 required for ray (see https://github.com/ray-project/ray/issues/31258) - numpy>=1.19.5,<1.24.0 + numpy>=1.19.5 psutil>=5.4.8 shapely>=2.0.0 tableprint>=0.9.1 @@ -35,8 +35,8 @@ install_requires = PyYAML>=3.13 twisted>=21.7.0 # for scenario requirements.txt files - pybullet>=3,<4.0 # planned to be made optional - cloudpickle>=1.3.0,<=2.1.0 # planned for removal + pybullet>=3,<4.0 # planned to be made optional (For >3.9 this requires python3.-dev) + cloudpickle>=1.3.0 # planned for removal [options.packages.find] exclude = @@ -52,7 +52,7 @@ argoverse = av2>=0.2.1 Rtree>=0.9.7 camera_obs = - Panda3D==1.10.9 + Panda3D>=1.10.13 panda3d-gltf==0.13 dev = black[jupyter]==22.6.0 @@ -86,8 +86,8 @@ opendrive = opendrive2lanelet>=1.2.1 Rtree>=0.9.7 rllib = - opencv-python==4.1.2.30 - opencv-python-headless==4.1.2.30 + opencv-python>=4.1.2.30,<5.0 + opencv-python-headless>=4.1.2.30,<5.0 ray[rllib]~=2.5.0 tensorflow-probability ray = @@ -96,7 +96,7 @@ ros = catkin_pkg rospkg sumo = - eclipse-sumo>=1.12.0 # sumo + eclipse-sumo>=1.12.0 Rtree>=0.9.7 # technically optional, but used by sumo internally for performance (see `getNeighboringLanes()`) test = # The following are for testing pytest>=6.2.5 @@ -109,8 +109,8 @@ test_notebook = jupyter-client>=7.1.2 pytest-notebook>=0.7.0 torch = - torch==1.4.0 - torchvision==0.5.0 + torch>=1.4.0 + torchvision>=0.5.0 train = tensorflow>=2.4.0 visdom = @@ -130,8 +130,8 @@ all = %(gif_recorder)s %(gymnasium)s %(opendrive)s + %(ray)s %(rllib)s - # %(ray)s # incompatible with [rllib] for now %(ros)s %(sumo)s %(test)s diff --git a/smarts/core/utils/episodes.py b/smarts/core/utils/episodes.py index b0f98473d8..a76b14aac2 100644 --- a/smarts/core/utils/episodes.py +++ b/smarts/core/utils/episodes.py @@ -200,16 +200,16 @@ class Episode: def __init__(self, episodes: Episodes): self._episodes = episodes - def continues(self, observation, reward, done, info) -> bool: + def continues(self, observation, reward, terminated, truncated, info) -> bool: """Determine if the current episode can continue.""" self._episodes.current_step += 1 if self._episodes.current_step >= self._episodes.max_steps: return False - if isinstance(done, dict): - return not done.get("__all__", all(done.values())) - return not done + if isinstance(terminated, dict): + return not terminated.get("__all__", all(terminated.values())) + return not terminated def episode_range(max_steps): diff --git a/smarts/engine.ini b/smarts/engine.ini index e65909b197..d28678a871 100644 --- a/smarts/engine.ini +++ b/smarts/engine.ini @@ -1,6 +1,6 @@ [benchmark] [core] -debug = false +debug = False observation_workers = 0 reset_retries = 0 [controllers] diff --git a/smarts/env/gymnasium/hiway_env_v1.py b/smarts/env/gymnasium/hiway_env_v1.py index fed4df8c64..497b73a246 100644 --- a/smarts/env/gymnasium/hiway_env_v1.py +++ b/smarts/env/gymnasium/hiway_env_v1.py @@ -428,7 +428,10 @@ def render( Note: Make sure that your class's :attr:`metadata` ``"render_modes"`` key includes the list of supported modes. """ - if "rgb_array" in self.metadata["render_modes"]: + if ( + "rgb_array" in self.metadata["render_modes"] + or self.render_mode == "rgb_array" + ): if self._env_renderer is None: from smarts.env.utils.record import AgentCameraRGBRender diff --git a/smarts/env/gymnasium/wrappers/metric/utils.py b/smarts/env/gymnasium/wrappers/metric/utils.py index ed1f4f4e15..84be0cfaf3 100644 --- a/smarts/env/gymnasium/wrappers/metric/utils.py +++ b/smarts/env/gymnasium/wrappers/metric/utils.py @@ -117,7 +117,7 @@ def nearest_waypoint( Returns: Tuple[Tuple[int, int], Optional[int]] : `matrix` index of shape (a,b) and scalar `point` index. """ - cur_point_index = ((np.int(1e10), np.int(1e10)), None) + cur_point_index = ((np.int32(1e10), np.int32(1e10)), None) if points.shape == (0,): return cur_point_index diff --git a/smarts/env/wrappers/record_video.py b/smarts/env/wrappers/record_video.py deleted file mode 100644 index 08fd0e311b..0000000000 --- a/smarts/env/wrappers/record_video.py +++ /dev/null @@ -1,168 +0,0 @@ -# MIT License -# -# Copyright (C) 2021. Huawei Technologies Co., Ltd. All rights reserved. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# The MIT License -# -# Copyright (c) 2016 OpenAI (https://openai.com) -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -# pytype: disable=annotation-type-mismatch -import os -from typing import Callable - -import gymnasium as gym -from gymnasium import logger -from gymnasium.wrappers.monitoring import video_recorder - - -def capped_cubic_video_schedule(episode_id, cap=1000): - """A utility to schedule frequency fall-off.""" - if episode_id < cap: - return int(round(episode_id ** (1.0 / 3))) ** 3 == episode_id - else: - return episode_id % cap == 0 - - -class RecordVideo(gym.Wrapper): - """A video recording wrapper.""" - - def __init__( - self, - env, - video_folder: str, - episode_trigger: Callable[[int], bool] = None, - step_trigger: Callable[[int], bool] = None, - video_length: int = 0, - name_prefix: str = "rl-video", - ): - super().__init__(env) - - if episode_trigger is None and step_trigger is None: - episode_trigger = capped_cubic_video_schedule - - trigger_count = sum(x is not None for x in [episode_trigger, step_trigger]) - assert trigger_count == 1, "Must specify exactly one trigger" - - self.episode_trigger = episode_trigger - self.step_trigger = step_trigger - self.video_recorder = None - - self.video_folder = os.path.abspath(video_folder) - # Create output folder if needed - if os.path.isdir(self.video_folder): - logger.warn( - f"Overwriting existing videos at {self.video_folder} folder (try specifying a different `video_folder` for the `RecordVideo` wrapper if this is not desired)" - ) - os.makedirs(self.video_folder, exist_ok=True) - - self.name_prefix = name_prefix - self.step_id = 0 - self.video_length = video_length - - self.recording = False - self.recorded_frames = 0 - self.is_vector_env = getattr(env, "is_vector_env", False) - self.episode_id = 0 - - def reset(self, **kwargs): - """Reset.""" - observations = super().reset(**kwargs) - if not self.recording and self._video_enabled(): - self.start_video_recorder() - return observations - - def start_video_recorder(self): - """Start video.""" - self.close_video_recorder() - - video_name = f"{self.name_prefix}-step-{self.step_id}" - if self.episode_trigger: - video_name = f"{self.name_prefix}-episode-{self.episode_id}" - - base_path = os.path.join(self.video_folder, video_name) - self.video_recorder = video_recorder.VideoRecorder( - env=self.env, - base_path=base_path, - metadata={"step_id": self.step_id, "episode_id": self.episode_id}, - ) - - self.video_recorder.capture_frame() - self.recorded_frames = 1 - self.recording = True - - def _video_enabled(self): - if self.step_trigger: - return self.step_trigger(self.step_id) - else: - return self.episode_trigger(self.episode_id) - - def step(self, action): - """Step.""" - observations, rewards, dones, infos = super().step(action) - - # increment steps and episodes - self.step_id += 1 - if not self.is_vector_env: - if dones: - self.episode_id += 1 - elif dones[0]: - self.episode_id += 1 - - if self.recording: - self.video_recorder.capture_frame() - self.recorded_frames += 1 - if self.video_length > 0: - if self.recorded_frames > self.video_length: - self.close_video_recorder() - else: - if not self.is_vector_env: - if dones: - self.close_video_recorder() - elif dones[0]: - self.close_video_recorder() - - elif self._video_enabled(): - self.start_video_recorder() - - return observations, rewards, dones, infos - - def close_video_recorder(self) -> None: - """Ends recording.""" - if self.recording: - self.video_recorder.close() - self.recording = False - self.recorded_frames = 1 - - def close(self): - """Close.""" - self.close_video_recorder() - - def __del__(self): - self.close_video_recorder() - - -# pytype: enable=annotation-type-mismatch