From 4b20cfa996a904ab9b2973b068f56695119e1730 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Tue, 4 Aug 2020 19:18:22 +0800 Subject: [PATCH 01/74] env code refactor --- tianshou/env/__init__.py | 7 +- tianshou/env/{vecenv/base.py => envs.py} | 120 +++++++++-- tianshou/env/vecenv/dummy.py | 65 ------ tianshou/env/vecenv/rayenv.py | 76 ------- tianshou/env/vecenv/subproc.py | 115 ----------- tianshou/env/worker/__init__.py | 0 tianshou/env/worker/base.py | 50 +++++ tianshou/env/worker/dummy.py | 44 +++++ tianshou/env/worker/ray.py | 44 +++++ .../{vecenv/shmemenv.py => worker/subproc.py} | 186 +++++++++--------- 10 files changed, 334 insertions(+), 373 deletions(-) rename tianshou/env/{vecenv/base.py => envs.py} (55%) delete mode 100644 tianshou/env/vecenv/dummy.py delete mode 100644 tianshou/env/vecenv/rayenv.py delete mode 100644 tianshou/env/vecenv/subproc.py create mode 100644 tianshou/env/worker/__init__.py create mode 100644 tianshou/env/worker/base.py create mode 100644 tianshou/env/worker/dummy.py create mode 100644 tianshou/env/worker/ray.py rename tianshou/env/{vecenv/shmemenv.py => worker/subproc.py} (51%) diff --git a/tianshou/env/__init__.py b/tianshou/env/__init__.py index 540fe1c48..165af8e03 100644 --- a/tianshou/env/__init__.py +++ b/tianshou/env/__init__.py @@ -1,9 +1,6 @@ -from tianshou.env.vecenv.base import BaseVectorEnv -from tianshou.env.vecenv.dummy import VectorEnv -from tianshou.env.vecenv.subproc import SubprocVectorEnv +from tianshou.env.envs import \ + (BaseVectorEnv, VectorEnv, SubprocVectorEnv, ShmemVectorEnv, RayVectorEnv) from tianshou.env.vecenv.asyncenv import AsyncVectorEnv -from tianshou.env.vecenv.rayenv import RayVectorEnv -from tianshou.env.vecenv.shmemenv import ShmemVectorEnv from tianshou.env.maenv import MultiAgentEnv __all__ = [ diff --git a/tianshou/env/vecenv/base.py b/tianshou/env/envs.py similarity index 55% rename from tianshou/env/vecenv/base.py rename to tianshou/env/envs.py index b6c160dab..eaa3afcda 100644 --- a/tianshou/env/vecenv/base.py +++ b/tianshou/env/envs.py @@ -1,10 +1,13 @@ import gym import numpy as np -from abc import ABC, abstractmethod -from typing import List, Tuple, Union, Optional, Callable +from typing import List, Tuple, Union, Optional, Callable, Any +from tianshou.env.worker.base import EnvWorker +from tianshou.env.worker.subproc import SubProcEnvWorker +from tianshou.env.worker.dummy import SequentialEnvWorker +from tianshou.env.worker.ray import RayEnvWorker -class BaseVectorEnv(ABC, gym.Env): +class BaseVectorEnv(gym.Env): """Base class for vectorized environments wrapper. Usage: :: @@ -40,8 +43,12 @@ def seed(self, seed): Otherwise, the outputs of these envs may be the same with each other. """ - def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: + def __init__(self, + env_fns: List[Callable[[], gym.Env]], + worker_fn: Callable[[Callable[[], gym.Env]], EnvWorker] + ) -> None: self._env_fns = env_fns + self.workers = [worker_fn(fn) for fn in env_fns] self.env_num = len(env_fns) def __len__(self) -> int: @@ -56,22 +63,24 @@ def __getattribute__(self, key: str): else: return self.__getattr__(key) - @abstractmethod def __getattr__(self, key: str): """Try to retrieve an attribute from each individual wrapped environment, if it does not belong to the wrapping vector environment class.""" - pass + return [getattr(worker, key) for worker in self.workers] - @abstractmethod def reset(self, id: Optional[Union[int, List[int]]] = None): """Reset the state of all the environments and return initial observations if id is ``None``, otherwise reset the specific environments with given id, either an int or a list. """ - pass + if id is None: + id = range(self.env_num) + elif np.isscalar(id): + id = [id] + obs = np.stack([self.workers[i].reset() for i in id]) + return obs - @abstractmethod def step(self, action: np.ndarray, id: Optional[Union[int, List[int]]] = None @@ -97,9 +106,15 @@ def step(self, * ``info`` a numpy.ndarray, contains auxiliary diagnostic \ information (helpful for debugging, and sometimes learning) """ - pass + if id is None: + id = range(self.env_num) + elif np.isscalar(id): + id = [id] + assert len(action) == len(id) + result = [self.workers[j].step(action[i]) for i, j in enumerate(id)] + obs, rew, done, info = map(np.stack, zip(*result)) + return obs, rew, done, info - @abstractmethod def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: """Set the seed for all environments. @@ -110,18 +125,85 @@ def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: generators. The first value in the list should be the "main" seed, or \ the value which a reproducer pass to "seed". """ - pass - - @abstractmethod - def render(self, **kwargs) -> None: + if np.isscalar(seed): + seed = [seed + _ for _ in range(self.env_num)] + elif seed is None: + seed = [seed] * self.env_num + result = [w.seed(s) for w, s in zip(self.workers, seed)] + return result + + def render(self, **kwargs) -> List[Any]: """Render all of the environments.""" - pass + return [w.render(**kwargs) for w in self.workers] - @abstractmethod - def close(self) -> None: + def close(self) -> List[Any]: """Close all of the environments. Environments will automatically close() themselves when garbage collected or when the program exits. """ - pass + return [w.close() for w in self.workers] + + +class VectorEnv(BaseVectorEnv): + def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: + super(VectorEnv, self).__init__(env_fns, + lambda fn: SequentialEnvWorker(fn)) + + +class SubprocVectorEnv(BaseVectorEnv): + """Vectorized environment wrapper based on subprocess. + + .. seealso:: + + Please refer to :class:`~tianshou.env.BaseVectorEnv` for more detailed + explanation. + """ + + def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: + super(SubprocVectorEnv, self).__init__( + env_fns, + lambda fn: SubProcEnvWorker(fn) + ) + + +class ShmemVectorEnv(BaseVectorEnv): + """Optimized version of SubprocVectorEnv that uses shared variables to + communicate observations. SubprocVectorEnv has exactly the same API as + SubprocVectorEnv. + + .. seealso:: + + Please refer to :class:`~tianshou.env.SubprocVectorEnv` for more + detailed explanation. + """ + + def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: + super(ShmemVectorEnv, self).__init__( + env_fns, + lambda fn: SubProcEnvWorker(fn, share_memory=True) + ) + + +class RayVectorEnv(BaseVectorEnv): + """Vectorized environment wrapper based on + `ray `_. This is a choice to run + distributed environments in a cluster. + + .. seealso:: + + Please refer to :class:`~tianshou.env.BaseVectorEnv` for more detailed + explanation. + """ + + def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: + try: + import ray + except ImportError as e: + raise ImportError( + 'Please install ray to support RayVectorEnv: pip install ray' + ) from e + + if not ray.is_initialized(): + ray.init() + super().__init__(env_fns, lambda fn: RayEnvWorker(fn)) diff --git a/tianshou/env/vecenv/dummy.py b/tianshou/env/vecenv/dummy.py deleted file mode 100644 index beece329c..000000000 --- a/tianshou/env/vecenv/dummy.py +++ /dev/null @@ -1,65 +0,0 @@ -import gym -import numpy as np -from typing import List, Tuple, Union, Optional, Callable, Any - -from tianshou.env import BaseVectorEnv - - -class VectorEnv(BaseVectorEnv): - """Dummy vectorized environment wrapper, implemented in for-loop. - - .. seealso:: - - Please refer to :class:`~tianshou.env.BaseVectorEnv` for more detailed - explanation. - """ - - def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: - super().__init__(env_fns) - self.envs = [_() for _ in env_fns] - - def __getattr__(self, key): - return [getattr(env, key) if hasattr(env, key) else None - for env in self.envs] - - def reset(self, id: Optional[Union[int, List[int]]] = None) -> np.ndarray: - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] - obs = np.stack([self.envs[i].reset() for i in id]) - return obs - - def step(self, - action: np.ndarray, - id: Optional[Union[int, List[int]]] = None - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] - assert len(action) == len(id) - result = [self.envs[i].step(action[i]) for i in id] - obs, rew, done, info = map(np.stack, zip(*result)) - return obs, rew, done, info - - def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: - if np.isscalar(seed): - seed = [seed + _ for _ in range(self.env_num)] - elif seed is None: - seed = [seed] * self.env_num - result = [] - for e, s in zip(self.envs, seed): - if hasattr(e, 'seed'): - result.append(e.seed(s)) - return result - - def render(self, **kwargs) -> List[Any]: - result = [] - for e in self.envs: - if hasattr(e, 'render'): - result.append(e.render(**kwargs)) - return result - - def close(self) -> List[Any]: - return [e.close() for e in self.envs] diff --git a/tianshou/env/vecenv/rayenv.py b/tianshou/env/vecenv/rayenv.py deleted file mode 100644 index 99707bbe2..000000000 --- a/tianshou/env/vecenv/rayenv.py +++ /dev/null @@ -1,76 +0,0 @@ -import gym -import numpy as np -from typing import List, Tuple, Union, Optional, Callable, Any - -try: - import ray -except ImportError: - pass - -from tianshou.env import BaseVectorEnv - - -class RayVectorEnv(BaseVectorEnv): - """Vectorized environment wrapper based on - `ray `_. This is a choice to run - distributed environments in a cluster. - - .. seealso:: - - Please refer to :class:`~tianshou.env.BaseVectorEnv` for more detailed - explanation. - """ - - def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: - super().__init__(env_fns) - try: - if not ray.is_initialized(): - ray.init() - except NameError: - raise ImportError( - 'Please install ray to support RayVectorEnv: pip install ray') - self.envs = [ - ray.remote(gym.Wrapper).options(num_cpus=0).remote(e()) - for e in env_fns] - - def __getattr__(self, key): - return ray.get([e.__getattr__.remote(key) for e in self.envs]) - - def step(self, - action: np.ndarray, - id: Optional[Union[int, List[int]]] = None - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] - assert len(action) == len(id) - result = ray.get([self.envs[j].step.remote(action[i]) - for i, j in enumerate(id)]) - obs, rew, done, info = map(np.stack, zip(*result)) - return obs, rew, done, info - - def reset(self, id: Optional[Union[int, List[int]]] = None) -> np.ndarray: - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] - obs = np.stack(ray.get([self.envs[i].reset.remote() for i in id])) - return obs - - def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: - if not hasattr(self.envs[0], 'seed'): - return [] - if np.isscalar(seed): - seed = [seed + _ for _ in range(self.env_num)] - elif seed is None: - seed = [seed] * self.env_num - return ray.get([e.seed.remote(s) for e, s in zip(self.envs, seed)]) - - def render(self, **kwargs) -> List[Any]: - if not hasattr(self.envs[0], 'render'): - return [None for e in self.envs] - return ray.get([e.render.remote(**kwargs) for e in self.envs]) - - def close(self) -> List[Any]: - return ray.get([e.close.remote() for e in self.envs]) diff --git a/tianshou/env/vecenv/subproc.py b/tianshou/env/vecenv/subproc.py deleted file mode 100644 index 9b8d8e2f3..000000000 --- a/tianshou/env/vecenv/subproc.py +++ /dev/null @@ -1,115 +0,0 @@ -import gym -import numpy as np -from multiprocessing import Process, Pipe -from typing import List, Tuple, Union, Optional, Callable, Any - -from tianshou.env import BaseVectorEnv -from tianshou.env.utils import CloudpickleWrapper - - -def _worker(parent, p, env_fn_wrapper): - parent.close() - env = env_fn_wrapper.data() - try: - while True: - cmd, data = p.recv() - if cmd == 'step': - p.send(env.step(data)) - elif cmd == 'reset': - p.send(env.reset()) - elif cmd == 'close': - p.send(env.close()) - p.close() - break - elif cmd == 'render': - p.send(env.render(**data) if hasattr(env, 'render') else None) - elif cmd == 'seed': - p.send(env.seed(data) if hasattr(env, 'seed') else None) - elif cmd == 'getattr': - p.send(getattr(env, data) if hasattr(env, data) else None) - else: - p.close() - raise NotImplementedError - except KeyboardInterrupt: - p.close() - - -class SubprocVectorEnv(BaseVectorEnv): - """Vectorized environment wrapper based on subprocess. - - .. seealso:: - - Please refer to :class:`~tianshou.env.BaseVectorEnv` for more detailed - explanation. - """ - - def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: - super().__init__(env_fns) - self.closed = False - self.parent_remote, self.child_remote = \ - zip(*[Pipe() for _ in range(self.env_num)]) - self.processes = [ - Process(target=_worker, args=( - parent, child, CloudpickleWrapper(env_fn)), daemon=True) - for (parent, child, env_fn) in zip( - self.parent_remote, self.child_remote, env_fns) - ] - for p in self.processes: - p.start() - for c in self.child_remote: - c.close() - - def __getattr__(self, key): - for p in self.parent_remote: - p.send(['getattr', key]) - return [p.recv() for p in self.parent_remote] - - def step(self, - action: np.ndarray, - id: Optional[Union[int, List[int]]] = None - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] - assert len(action) == len(id) - for i, j in enumerate(id): - self.parent_remote[j].send(['step', action[i]]) - result = [self.parent_remote[i].recv() for i in id] - obs, rew, done, info = map(np.stack, zip(*result)) - return obs, rew, done, info - - def reset(self, id: Optional[Union[int, List[int]]] = None) -> np.ndarray: - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] - for i in id: - self.parent_remote[i].send(['reset', None]) - obs = np.stack([self.parent_remote[i].recv() for i in id]) - return obs - - def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: - if np.isscalar(seed): - seed = [seed + _ for _ in range(self.env_num)] - elif seed is None: - seed = [seed] * self.env_num - for p, s in zip(self.parent_remote, seed): - p.send(['seed', s]) - return [p.recv() for p in self.parent_remote] - - def render(self, **kwargs) -> List[Any]: - for p in self.parent_remote: - p.send(['render', kwargs]) - return [p.recv() for p in self.parent_remote] - - def close(self) -> List[Any]: - if self.closed: - return [] - for p in self.parent_remote: - p.send(['close', None]) - result = [p.recv() for p in self.parent_remote] - self.closed = True - for p in self.processes: - p.join() - return result diff --git a/tianshou/env/worker/__init__.py b/tianshou/env/worker/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py new file mode 100644 index 000000000..95a50fe1b --- /dev/null +++ b/tianshou/env/worker/base.py @@ -0,0 +1,50 @@ +import gym +import numpy as np +from abc import ABC, abstractmethod +from typing import List, Tuple, Optional, Callable, Any + + +class EnvWorker(ABC, gym.Env): + """An abstract worker for an environment. + """ + + def __init__(self, env_fn: Callable[[], gym.Env]) -> None: + self._env_fn = env_fn + + def __getattribute__(self, key: str): + if key not in ('observation_space', 'action_space'): + return super().__getattribute__(key) + else: + return self.__getattr__(key) + + @abstractmethod + def __getattr__(self, key: str): + pass + + @abstractmethod + def reset(self): + pass + + @abstractmethod + def step(self, action: np.ndarray + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + pass + + @abstractmethod + def seed(self, seed: Optional[int] = None): + pass + + @abstractmethod + def render(self, **kwargs) -> None: + pass + + @abstractmethod + def close(self) -> Any: + pass + + @staticmethod + def wait(workers: List['EnvWorker']) -> List['EnvWorker']: + """ + Given a list of workers, return those ready ones. + """ + raise NotImplementedError diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py new file mode 100644 index 000000000..3d301bf31 --- /dev/null +++ b/tianshou/env/worker/dummy.py @@ -0,0 +1,44 @@ +from typing import List, Callable, Tuple, Optional, Any + +import gym +import numpy as np + +from tianshou.env.worker.base import EnvWorker + + +class SequentialEnvWorker(EnvWorker): + """ + Dummy worker used in sequential vector environments + """ + + @staticmethod + def wait(workers: List['SequentialEnvWorker'] + ) -> List['SequentialEnvWorker']: + # SequentialEnvWorker objects are always ready + return workers + + def __init__(self, env_fn: Callable[[], gym.Env]) -> None: + super(SequentialEnvWorker, self).__init__(env_fn) + self.env = env_fn() + + def __getattr__(self, key: str): + if hasattr(self.env, key): + return getattr(self.env, key) + return None + + def reset(self): + return self.env.reset() + + def step(self, action: np.ndarray + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + return self.env.step(action) + + def seed(self, seed: Optional[int] = None): + return self.env.seed(seed) if hasattr(self.env, 'seed') else None + + def render(self, **kwargs) -> None: + return self.env.render(**kwargs) if \ + hasattr(self.env, 'render') else None + + def close(self) -> Any: + return self.env.close() diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py new file mode 100644 index 000000000..27ffdf97f --- /dev/null +++ b/tianshou/env/worker/ray.py @@ -0,0 +1,44 @@ +from typing import List, Callable, Tuple, Optional, Any + +import gym +import numpy as np +import ray + +from tianshou.env.worker.base import EnvWorker + + +class RayEnvWorker(EnvWorker): + @staticmethod + def wait(workers: List['RayEnvWorker']) -> List['RayEnvWorker']: + ready_envs, _ = ray.wait( + [x.env for x in workers], + num_returns=len(workers), + timeout=0) + return [workers[ready_envs.index(env)] for env in ready_envs] + + def __init__(self, env_fn: Callable[[], gym.Env]) -> None: + super(RayEnvWorker, self).__init__(env_fn) + self.env = ray.remote(gym.Wrapper).options(num_cpus=0).remote(env_fn()) + + def __getattr__(self, key: str): + return ray.get(self.env.__getattr__.remote(key)) + + def reset(self): + return ray.get(self.env.reset.remote()) + + def step(self, action: np.ndarray + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + return ray.get(self.env.step.remote(action)) + + def seed(self, seed: Optional[int] = None): + if hasattr(self.env, 'seed'): + return ray.get(self.env.seed.remote(seed)) + return None + + def render(self, **kwargs) -> None: + if hasattr(self.env, 'render'): + return ray.get(self.env.render.remote(**kwargs)) + return None + + def close(self) -> Any: + return ray.get(self.env.close.remote()) diff --git a/tianshou/env/vecenv/shmemenv.py b/tianshou/env/worker/subproc.py similarity index 51% rename from tianshou/env/vecenv/shmemenv.py rename to tianshou/env/worker/subproc.py index a764ba6d3..f471d7d13 100644 --- a/tianshou/env/vecenv/shmemenv.py +++ b/tianshou/env/worker/subproc.py @@ -1,30 +1,17 @@ -import gym import ctypes -import numpy as np from collections import OrderedDict -from multiprocessing import Pipe, Process, Array -from typing import Callable, List, Optional, Tuple, Union +from multiprocessing import Array, Pipe, connection +from multiprocessing.context import Process +from typing import Callable, Any, List, Tuple, Optional +import gym +import numpy as np -from tianshou.env import BaseVectorEnv, SubprocVectorEnv from tianshou.env.utils import CloudpickleWrapper +from tianshou.env.worker.base import EnvWorker -_NP_TO_CT = {np.bool: ctypes.c_bool, - np.bool_: ctypes.c_bool, - np.uint8: ctypes.c_uint8, - np.uint16: ctypes.c_uint16, - np.uint32: ctypes.c_uint32, - np.uint64: ctypes.c_uint64, - np.int8: ctypes.c_int8, - np.int16: ctypes.c_int16, - np.int32: ctypes.c_int32, - np.int64: ctypes.c_int64, - np.float32: ctypes.c_float, - np.float64: ctypes.c_double} - -def _shmem_worker(parent, p, env_fn_wrapper, obs_bufs): - """Control a single environment instance using IPC and shared memory.""" +def _worker(parent, p, env_fn_wrapper, obs_bufs=None): def _encode_obs(obs, buffer): if isinstance(obs, np.ndarray): buffer.save(obs) @@ -42,9 +29,14 @@ def _encode_obs(obs, buffer): cmd, data = p.recv() if cmd == 'step': obs, reward, done, info = env.step(data) - p.send((_encode_obs(obs, obs_bufs), reward, done, info)) + if obs_bufs is not None: + obs = _encode_obs(obs, obs_bufs) + p.send((obs, reward, done, info)) elif cmd == 'reset': - p.send(_encode_obs(env.reset(), obs_bufs)) + obs = env.reset() + if obs_bufs is not None: + obs = _encode_obs(obs, obs_bufs) + p.send(obs) elif cmd == 'close': p.send(env.close()) p.close() @@ -62,6 +54,20 @@ def _encode_obs(obs, buffer): p.close() +_NP_TO_CT = {np.bool: ctypes.c_bool, + np.bool_: ctypes.c_bool, + np.uint8: ctypes.c_uint8, + np.uint16: ctypes.c_uint16, + np.uint32: ctypes.c_uint32, + np.uint64: ctypes.c_uint64, + np.int8: ctypes.c_int8, + np.int16: ctypes.c_int16, + np.int32: ctypes.c_int32, + np.int64: ctypes.c_int64, + np.float32: ctypes.c_float, + np.float64: ctypes.c_double} + + class ShArray: """Wrapper of multiprocessing Array""" @@ -81,90 +87,42 @@ def get(self): dtype=self.dtype).reshape(self.shape) -class ShmemVectorEnv(SubprocVectorEnv): - """Optimized version of SubprocVectorEnv that uses shared variables to - communicate observations. SubprocVectorEnv has exactly the same API as - SubprocVectorEnv. - - .. seealso:: - - Please refer to :class:`~tianshou.env.SubprocVectorEnv` for more - detailed explanation. - - ShmemVectorEnv Class was inspired by openai baseline's implementation. - Please refer to 'https://github.com/openai/baselines/blob/master/baselines/ - common/vec_env/shmem_vec_env.py' for more info if you are interested. - """ - - def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: - BaseVectorEnv.__init__(self, env_fns) - # Mind that SubprocVectorEnv is not initialised. +class SubProcEnvWorker(EnvWorker): + + def __init__(self, env_fn: Callable[[], gym.Env], share_memory=False + ) -> None: + super(SubProcEnvWorker, self).__init__(env_fn) + self.parent_remote, self.child_remote = Pipe() + self.share_memory = share_memory + self.buffer = None + if self.share_memory: + dummy = env_fn() + obs_space = dummy.observation_space + dummy.close() + del dummy + self.buffer = SubProcEnvWorker._setup_buf(obs_space) + args = (self.parent_remote, self.child_remote, + CloudpickleWrapper(env_fn), self.buffer) + self.process = Process(target=_worker, args=args, daemon=True) + self.process.start() + self.child_remote.close() self.closed = False - dummy = env_fns[0]() - obs_space = dummy.observation_space - dummy.close() - del dummy - self.obs_bufs = [ShmemVectorEnv._setup_buf(obs_space) - for _ in range(self.env_num)] - self.parent_remote, self.child_remote = \ - zip(*[Pipe() for _ in range(self.env_num)]) - self.processes = [ - Process(target=_shmem_worker, args=( - parent, child, CloudpickleWrapper(env_fn), - obs_buf), daemon=True) - for (parent, child, env_fn, obs_buf) in zip( - self.parent_remote, self.child_remote, env_fns, self.obs_bufs) - ] - for p in self.processes: - p.start() - for c in self.child_remote: - c.close() - - def step(self, - action: np.ndarray, - id: Optional[Union[int, List[int]]] = None - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] - assert len(action) == len(id) - for i, j in enumerate(id): - self.parent_remote[j].send(['step', action[i]]) - result = [] - for i in id: - obs, rew, done, info = self.parent_remote[i].recv() - obs = self._decode_obs(obs, i) - result.append((obs, rew, done, info)) - obs, rew, done, info = map(np.stack, zip(*result)) - return obs, rew, done, info - - def reset(self, id: Optional[Union[int, List[int]]] = None) -> np.ndarray: - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] - for i in id: - self.parent_remote[i].send(['reset', None]) - obs = np.stack( - [self._decode_obs(self.parent_remote[i].recv(), i) for i in id]) - return obs @staticmethod def _setup_buf(space): if isinstance(space, gym.spaces.Dict): assert isinstance(space.spaces, OrderedDict) - buffer = {k: ShmemVectorEnv._setup_buf(v) + buffer = {k: SubProcEnvWorker._setup_buf(v) for k, v in space.spaces.items()} elif isinstance(space, gym.spaces.Tuple): assert isinstance(space.spaces, tuple) - buffer = tuple([ShmemVectorEnv._setup_buf(t) + buffer = tuple([SubProcEnvWorker._setup_buf(t) for t in space.spaces]) else: buffer = ShArray(space.dtype, space.shape) return buffer - def _decode_obs(self, isNone, index): + def _decode_obs(self, isNone): def decode_obs(buffer): if isinstance(buffer, ShArray): return buffer.get() @@ -174,4 +132,46 @@ def decode_obs(buffer): return {k: decode_obs(v) for k, v in buffer.items()} else: raise NotImplementedError - return decode_obs(self.obs_bufs[index]) + return decode_obs(self.buffer) + + def render(self, **kwargs) -> None: + self.parent_remote.send(['render', kwargs]) + return self.parent_remote.recv() + + def close(self) -> Any: + if self.closed: + return [] + self.parent_remote.send(['close', None]) + result = self.parent_remote.recv() + self.closed = True + self.process.join() + return result + + @staticmethod + def wait(workers: List['SubProcEnvWorker']) -> List['SubProcEnvWorker']: + conns = [x.parent_remote for x in workers] + ready_conns = connection.wait(conns) + return [workers[conns.index(con)] for con in ready_conns] + + def __getattr__(self, key: str): + self.parent_remote.send(['getattr', key]) + return self.parent_remote.recv() + + def step(self, action: np.ndarray + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + self.parent_remote.send(['step', action]) + obs, rew, done, info = self.parent_remote.recv() + if self.share_memory: + obs = self._decode_obs(obs) + return obs, rew, done, info + + def reset(self): + self.parent_remote.send(['reset', None]) + obs = self.parent_remote.recv() + if self.share_memory: + obs = self._decode_obs(obs) + return obs + + def seed(self, seed: Optional[int] = None): + self.parent_remote.send(['seed', seed]) + return self.parent_remote.recv() From a2a543ac3d3719d1a53a6bf73b67f8c759ab317d Mon Sep 17 00:00:00 2001 From: youkaichao Date: Tue, 4 Aug 2020 21:11:14 +0800 Subject: [PATCH 02/74] enable async simulation for all vec env --- test/base/test_collector.py | 4 +- test/base/test_env.py | 6 +- tianshou/data/collector.py | 4 +- tianshou/env/__init__.py | 4 +- tianshou/env/envs.py | 209 -------------------------------- tianshou/env/vecenv/__init__.py | 0 tianshou/env/vecenv/asyncenv.py | 104 ---------------- tianshou/env/worker/base.py | 16 ++- tianshou/env/worker/dummy.py | 6 +- tianshou/env/worker/ray.py | 10 +- tianshou/env/worker/subproc.py | 12 +- tianshou/utils/__init__.py | 18 +++ 12 files changed, 56 insertions(+), 337 deletions(-) delete mode 100644 tianshou/env/envs.py delete mode 100644 tianshou/env/vecenv/__init__.py delete mode 100644 tianshou/env/vecenv/asyncenv.py diff --git a/test/base/test_collector.py b/test/base/test_collector.py index 68cf5c74c..19c724988 100644 --- a/test/base/test_collector.py +++ b/test/base/test_collector.py @@ -2,7 +2,7 @@ from torch.utils.tensorboard import SummaryWriter from tianshou.policy import BasePolicy -from tianshou.env import VectorEnv, SubprocVectorEnv, AsyncVectorEnv +from tianshou.env import VectorEnv, SubprocVectorEnv from tianshou.data import Collector, Batch, ReplayBuffer if __name__ == '__main__': @@ -116,7 +116,7 @@ def test_collector_with_async(): env_fns = [lambda x=i: MyTestEnv(size=x, sleep=0.1, random_sleep=True) for i in env_lens] - venv = AsyncVectorEnv(env_fns) + venv = SubprocVectorEnv(env_fns) policy = MyPolicy() c1 = Collector(policy, venv, ReplayBuffer(size=1000, ignore_obs_next=False), diff --git a/test/base/test_env.py b/test/base/test_env.py index be876699c..6e37c638c 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -3,7 +3,7 @@ from gym.spaces.discrete import Discrete from tianshou.data import Batch from tianshou.env import VectorEnv, SubprocVectorEnv, \ - RayVectorEnv, AsyncVectorEnv, ShmemVectorEnv + RayVectorEnv, ShmemVectorEnv if __name__ == '__main__': from env import MyTestEnv @@ -36,7 +36,7 @@ def test_async_env(num=8, sleep=0.1): lambda i=i: MyTestEnv(size=i, sleep=sleep, random_sleep=True) for i in range(size, size + num) ] - v = AsyncVectorEnv(env_fns, wait_num=num // 2) + v = SubprocVectorEnv(env_fns, wait_num=num // 2) v.seed() v.reset() # for a random variable u ~ U[0, 1], let v = max{u1, u2, ..., un} @@ -44,7 +44,7 @@ def test_async_env(num=8, sleep=0.1): # expectation of v is n / (n + 1) # for a synchronous environment, the following actions should take # about 7 * sleep * num / (num + 1) seconds - # for AsyncVectorEnv, the analysis is complicated, but the time cost + # for async simulation, the analysis is complicated, but the time cost # should be smaller action_list = [1] * num + [0] * (num * 2) + [1] * (num * 4) current_index_start = 0 diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index 9105374bf..6d4a11cfa 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -5,7 +5,7 @@ import numpy as np from typing import Any, Dict, List, Union, Optional, Callable -from tianshou.env import BaseVectorEnv, VectorEnv, AsyncVectorEnv +from tianshou.env import BaseVectorEnv, VectorEnv from tianshou.policy import BasePolicy from tianshou.exploration import BaseNoise from tianshou.data import Batch, ReplayBuffer, ListReplayBuffer, to_numpy @@ -103,7 +103,7 @@ def __init__(self, self._ready_env_ids = np.arange(self.env_num) # self.async is a flag to indicate whether this collector works # with asynchronous simulation - self.is_async = isinstance(env, AsyncVectorEnv) + self.is_async = env.is_async # need cache buffers before storing in the main buffer self._cached_buf = [ListReplayBuffer() for _ in range(self.env_num)] self.collect_time, self.collect_step, self.collect_episode = 0., 0, 0 diff --git a/tianshou/env/__init__.py b/tianshou/env/__init__.py index 165af8e03..3429a630c 100644 --- a/tianshou/env/__init__.py +++ b/tianshou/env/__init__.py @@ -1,13 +1,11 @@ -from tianshou.env.envs import \ +from tianshou.env.venvs import \ (BaseVectorEnv, VectorEnv, SubprocVectorEnv, ShmemVectorEnv, RayVectorEnv) -from tianshou.env.vecenv.asyncenv import AsyncVectorEnv from tianshou.env.maenv import MultiAgentEnv __all__ = [ 'BaseVectorEnv', 'VectorEnv', 'SubprocVectorEnv', - 'AsyncVectorEnv', 'RayVectorEnv', 'ShmemVectorEnv', 'MultiAgentEnv', diff --git a/tianshou/env/envs.py b/tianshou/env/envs.py deleted file mode 100644 index eaa3afcda..000000000 --- a/tianshou/env/envs.py +++ /dev/null @@ -1,209 +0,0 @@ -import gym -import numpy as np -from typing import List, Tuple, Union, Optional, Callable, Any -from tianshou.env.worker.base import EnvWorker -from tianshou.env.worker.subproc import SubProcEnvWorker -from tianshou.env.worker.dummy import SequentialEnvWorker -from tianshou.env.worker.ray import RayEnvWorker - - -class BaseVectorEnv(gym.Env): - """Base class for vectorized environments wrapper. Usage: - :: - - env_num = 8 - envs = VectorEnv([lambda: gym.make(task) for _ in range(env_num)]) - assert len(envs) == env_num - - It accepts a list of environment generators. In other words, an environment - generator ``efn`` of a specific task means that ``efn()`` returns the - environment of the given task, for example, ``gym.make(task)``. - - All of the VectorEnv must inherit :class:`~tianshou.env.BaseVectorEnv`. - Here are some other usages: - :: - - envs.seed(2) # which is equal to the next line - envs.seed([2, 3, 4, 5, 6, 7, 8, 9]) # set specific seed for each env - obs = envs.reset() # reset all environments - obs = envs.reset([0, 5, 7]) # reset 3 specific environments - obs, rew, done, info = envs.step([1] * 8) # step synchronously - envs.render() # render all environments - envs.close() # close all environments - - .. warning:: - - If you use your own environment, please make sure the ``seed`` method - is set up properly, e.g., - :: - - def seed(self, seed): - np.random.seed(seed) - - Otherwise, the outputs of these envs may be the same with each other. - """ - - def __init__(self, - env_fns: List[Callable[[], gym.Env]], - worker_fn: Callable[[Callable[[], gym.Env]], EnvWorker] - ) -> None: - self._env_fns = env_fns - self.workers = [worker_fn(fn) for fn in env_fns] - self.env_num = len(env_fns) - - def __len__(self) -> int: - """Return len(self), which is the number of environments.""" - return self.env_num - - def __getattribute__(self, key: str): - """Switch between the default attribute getter or one - looking at wrapped environment level depending on the key.""" - if key not in ('observation_space', 'action_space'): - return super().__getattribute__(key) - else: - return self.__getattr__(key) - - def __getattr__(self, key: str): - """Try to retrieve an attribute from each individual wrapped - environment, if it does not belong to the wrapping vector - environment class.""" - return [getattr(worker, key) for worker in self.workers] - - def reset(self, id: Optional[Union[int, List[int]]] = None): - """Reset the state of all the environments and return initial - observations if id is ``None``, otherwise reset the specific - environments with given id, either an int or a list. - """ - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] - obs = np.stack([self.workers[i].reset() for i in id]) - return obs - - def step(self, - action: np.ndarray, - id: Optional[Union[int, List[int]]] = None - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - """Run one timestep of all the environments’ dynamics if id is - ``None``, otherwise run one timestep for some environments - with given id, either an int or a list. When the end of - episode is reached, you are responsible for calling reset(id) - to reset this environment’s state. - - Accept a batch of action and return a tuple (obs, rew, done, info). - - :param numpy.ndarray action: a batch of action provided by the agent. - - :return: A tuple including four items: - - * ``obs`` a numpy.ndarray, the agent's observation of current \ - environments - * ``rew`` a numpy.ndarray, the amount of rewards returned after \ - previous actions - * ``done`` a numpy.ndarray, whether these episodes have ended, in \ - which case further step() calls will return undefined results - * ``info`` a numpy.ndarray, contains auxiliary diagnostic \ - information (helpful for debugging, and sometimes learning) - """ - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] - assert len(action) == len(id) - result = [self.workers[j].step(action[i]) for i, j in enumerate(id)] - obs, rew, done, info = map(np.stack, zip(*result)) - return obs, rew, done, info - - def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: - """Set the seed for all environments. - - Accept ``None``, an int (which will extend ``i`` to - ``[i, i + 1, i + 2, ...]``) or a list. - - :return: The list of seeds used in this env's random number \ - generators. The first value in the list should be the "main" seed, or \ - the value which a reproducer pass to "seed". - """ - if np.isscalar(seed): - seed = [seed + _ for _ in range(self.env_num)] - elif seed is None: - seed = [seed] * self.env_num - result = [w.seed(s) for w, s in zip(self.workers, seed)] - return result - - def render(self, **kwargs) -> List[Any]: - """Render all of the environments.""" - return [w.render(**kwargs) for w in self.workers] - - def close(self) -> List[Any]: - """Close all of the environments. - - Environments will automatically close() themselves when garbage - collected or when the program exits. - """ - return [w.close() for w in self.workers] - - -class VectorEnv(BaseVectorEnv): - def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: - super(VectorEnv, self).__init__(env_fns, - lambda fn: SequentialEnvWorker(fn)) - - -class SubprocVectorEnv(BaseVectorEnv): - """Vectorized environment wrapper based on subprocess. - - .. seealso:: - - Please refer to :class:`~tianshou.env.BaseVectorEnv` for more detailed - explanation. - """ - - def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: - super(SubprocVectorEnv, self).__init__( - env_fns, - lambda fn: SubProcEnvWorker(fn) - ) - - -class ShmemVectorEnv(BaseVectorEnv): - """Optimized version of SubprocVectorEnv that uses shared variables to - communicate observations. SubprocVectorEnv has exactly the same API as - SubprocVectorEnv. - - .. seealso:: - - Please refer to :class:`~tianshou.env.SubprocVectorEnv` for more - detailed explanation. - """ - - def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: - super(ShmemVectorEnv, self).__init__( - env_fns, - lambda fn: SubProcEnvWorker(fn, share_memory=True) - ) - - -class RayVectorEnv(BaseVectorEnv): - """Vectorized environment wrapper based on - `ray `_. This is a choice to run - distributed environments in a cluster. - - .. seealso:: - - Please refer to :class:`~tianshou.env.BaseVectorEnv` for more detailed - explanation. - """ - - def __init__(self, env_fns: List[Callable[[], gym.Env]]) -> None: - try: - import ray - except ImportError as e: - raise ImportError( - 'Please install ray to support RayVectorEnv: pip install ray' - ) from e - - if not ray.is_initialized(): - ray.init() - super().__init__(env_fns, lambda fn: RayEnvWorker(fn)) diff --git a/tianshou/env/vecenv/__init__.py b/tianshou/env/vecenv/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tianshou/env/vecenv/asyncenv.py b/tianshou/env/vecenv/asyncenv.py deleted file mode 100644 index 00d1e51ca..000000000 --- a/tianshou/env/vecenv/asyncenv.py +++ /dev/null @@ -1,104 +0,0 @@ -import gym -import numpy as np -from multiprocessing import connection -from typing import List, Tuple, Union, Optional, Callable, Any - -from tianshou.env import SubprocVectorEnv - - -class AsyncVectorEnv(SubprocVectorEnv): - """Vectorized asynchronous environment wrapper based on subprocess. - - :param wait_num: used in asynchronous simulation if the time cost of - ``env.step`` varies with time and synchronously waiting for all - environments to finish a step is time-wasting. In that case, we can - return when ``wait_num`` environments finish a step and keep on - simulation in these environments. If ``None``, asynchronous simulation - is disabled; else, ``1 <= wait_num <= env_num``. - - .. seealso:: - - Please refer to :class:`~tianshou.env.BaseVectorEnv` for more detailed - explanation. - """ - - def __init__(self, env_fns: List[Callable[[], gym.Env]], - wait_num: Optional[int] = None) -> None: - super().__init__(env_fns) - self.wait_num = wait_num or len(env_fns) - assert 1 <= self.wait_num <= len(env_fns), \ - f'wait_num should be in [1, {len(env_fns)}], but got {wait_num}' - self.waiting_conn = [] - # environments in self.ready_id is actually ready - # but environments in self.waiting_id are just waiting when checked, - # and they may be ready now, but this is not known until we check it - # in the step() function - self.waiting_id = [] - # all environments are ready in the beginning - self.ready_id = list(range(self.env_num)) - - def _assert_and_transform_id(self, - id: Optional[Union[int, List[int]]] = None - ) -> List[int]: - if id is None: - id = list(range(self.env_num)) - elif np.isscalar(id): - id = [id] - for i in id: - assert i not in self.waiting_id, \ - f'Cannot reset environment {i} which is stepping now!' - assert i in self.ready_id, \ - f'Can only reset ready environments {self.ready_id}.' - return id - - def reset(self, id: Optional[Union[int, List[int]]] = None) -> np.ndarray: - id = self._assert_and_transform_id(id) - return super().reset(id) - - def render(self, **kwargs) -> List[Any]: - if len(self.waiting_id) > 0: - raise RuntimeError( - f"Environments {self.waiting_id} are still " - f"stepping, cannot render them now.") - return super().render(**kwargs) - - def close(self) -> List[Any]: - if self.closed: - return [] - # finish remaining steps, and close - self.step(None) - return super().close() - - def step(self, - action: Optional[np.ndarray], - id: Optional[Union[int, List[int]]] = None - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - """ - Provide the given action to the environments. The action sequence - should correspond to the ``id`` argument, and the ``id`` argument - should be a subset of the ``env_id`` in the last returned ``info`` - (initially they are env_ids of all the environments). If action is - ``None``, fetch unfinished step() calls instead. - """ - if action is not None: - id = self._assert_and_transform_id(id) - assert len(action) == len(id) - for i, (act, env_id) in enumerate(zip(action, id)): - self.parent_remote[env_id].send(['step', act]) - self.waiting_conn.append(self.parent_remote[env_id]) - self.waiting_id.append(env_id) - self.ready_id = [x for x in self.ready_id if x not in id] - result = [] - while len(self.waiting_conn) > 0 and len(result) < self.wait_num: - ready_conns = connection.wait(self.waiting_conn) - for conn in ready_conns: - waiting_index = self.waiting_conn.index(conn) - self.waiting_conn.pop(waiting_index) - env_id = self.waiting_id.pop(waiting_index) - ans = conn.recv() - obs, rew, done, info = ans - info["env_id"] = env_id - result.append((obs, rew, done, info)) - self.ready_id.append(env_id) - obs, rew, done, info = map(np.stack, zip(*result)) - return obs, rew, done, info diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 95a50fe1b..18532e77a 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -25,10 +25,24 @@ def __getattr__(self, key: str): def reset(self): pass + def send_action(self, action: np.ndarray): + self.action = action + @abstractmethod + def get_result(self + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + pass + def step(self, action: np.ndarray ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - pass + """ + ``send_action`` and ``get_result`` are coupled in sync simulation, + so typically users only call ``step`` function. But they can be called + separately in async simulation, i.e. someone calls ``send_action`` + first, and calls ``get_result`` later. + """ + self.send_action(action) + return self.get_result() @abstractmethod def seed(self, seed: Optional[int] = None): diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index 3d301bf31..29c676bb9 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -29,9 +29,9 @@ def __getattr__(self, key: str): def reset(self): return self.env.reset() - def step(self, action: np.ndarray - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - return self.env.step(action) + def get_result(self + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + return self.env.step(self.action) def seed(self, seed: Optional[int] = None): return self.env.seed(seed) if hasattr(self.env, 'seed') else None diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index 27ffdf97f..cdbc7b5c6 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -26,9 +26,13 @@ def __getattr__(self, key: str): def reset(self): return ray.get(self.env.reset.remote()) - def step(self, action: np.ndarray - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - return ray.get(self.env.step.remote(action)) + def get_result(self + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + return ray.get(self.action) + + def send_action(self, action: np.ndarray): + # self.action is actually a handle + self.action = self.env.step.remote(action) def seed(self, seed: Optional[int] = None): if hasattr(self.env, 'seed'): diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index f471d7d13..8b0fab4ac 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -106,7 +106,6 @@ def __init__(self, env_fn: Callable[[], gym.Env], share_memory=False self.process = Process(target=_worker, args=args, daemon=True) self.process.start() self.child_remote.close() - self.closed = False @staticmethod def _setup_buf(space): @@ -139,11 +138,8 @@ def render(self, **kwargs) -> None: return self.parent_remote.recv() def close(self) -> Any: - if self.closed: - return [] self.parent_remote.send(['close', None]) result = self.parent_remote.recv() - self.closed = True self.process.join() return result @@ -157,14 +153,16 @@ def __getattr__(self, key: str): self.parent_remote.send(['getattr', key]) return self.parent_remote.recv() - def step(self, action: np.ndarray - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - self.parent_remote.send(['step', action]) + def get_result(self + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: obs, rew, done, info = self.parent_remote.recv() if self.share_memory: obs = self._decode_obs(obs) return obs, rew, done, info + def send_action(self, action: np.ndarray): + self.parent_remote.send(['step', action]) + def reset(self): self.parent_remote.send(['reset', None]) obs = self.parent_remote.recv() diff --git a/tianshou/utils/__init__.py b/tianshou/utils/__init__.py index 95736dabc..eabe1190f 100644 --- a/tianshou/utils/__init__.py +++ b/tianshou/utils/__init__.py @@ -1,7 +1,25 @@ from tianshou.utils.config import tqdm_config from tianshou.utils.moving_average import MovAvg + +def run_once(f): + """ + Run once decorator for a method in a class. Each instance can run + the method at most once. + """ + f.has_run_objects = set() + + def wrapper(self, *args, **kwargs): + if hash(self) in f.has_run_objects: + raise RuntimeError( + f'{f} can be called only once for object {self}') + f.has_run_objects.add(hash(self)) + return f(self, *args, **kwargs) + return wrapper + + __all__ = [ 'MovAvg', + 'run_once', 'tqdm_config', ] From 7e50d22dcf0a5bc7d710173cd2bf0fe67049b1d8 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Tue, 4 Aug 2020 21:31:09 +0800 Subject: [PATCH 03/74] add venvs --- tianshou/env/venvs.py | 319 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 tianshou/env/venvs.py diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py new file mode 100644 index 000000000..094d67d20 --- /dev/null +++ b/tianshou/env/venvs.py @@ -0,0 +1,319 @@ +import gym +import numpy as np +from typing import List, Tuple, Union, Optional, Callable, Any +from tianshou.env.worker.base import EnvWorker +from tianshou.env.worker.subproc import SubProcEnvWorker +from tianshou.env.worker.dummy import SequentialEnvWorker +from tianshou.env.worker.ray import RayEnvWorker +from tianshou.utils import run_once + + +class BaseVectorEnv(gym.Env): + """Base class for vectorized environments wrapper. Usage: + :: + + env_num = 8 + envs = VectorEnv([lambda: gym.make(task) for _ in range(env_num)]) + assert len(envs) == env_num + + It accepts a list of environment generators. In other words, an environment + generator ``efn`` of a specific task means that ``efn()`` returns the + environment of the given task, for example, ``gym.make(task)``. + + All of the VectorEnv must inherit :class:`~tianshou.env.BaseVectorEnv`. + Here are some other usages: + :: + + envs.seed(2) # which is equal to the next line + envs.seed([2, 3, 4, 5, 6, 7, 8, 9]) # set specific seed for each env + obs = envs.reset() # reset all environments + obs = envs.reset([0, 5, 7]) # reset 3 specific environments + obs, rew, done, info = envs.step([1] * 8) # step synchronously + envs.render() # render all environments + envs.close() # close all environments + + .. warning:: + + If you use your own environment, please make sure the ``seed`` method + is set up properly, e.g., + :: + + def seed(self, seed): + np.random.seed(seed) + + Otherwise, the outputs of these envs may be the same with each other. + + + :param wait_num: used in asynchronous simulation if the time cost of + ``env.step`` varies with time and synchronously waiting for all + environments to finish a step is time-wasting. In that case, we can + return when ``wait_num`` environments finish a step and keep on + simulation in these environments. If ``None``, asynchronous simulation + is disabled; else, ``1 <= wait_num <= env_num``. + + """ + + def __init__(self, + env_fns: List[Callable[[], gym.Env]], + worker_fn: Callable[[Callable[[], gym.Env]], EnvWorker], + wait_num: Optional[int] = None, + ) -> None: + self._env_fns = env_fns + self.workers = [worker_fn(fn) for fn in env_fns] + self.env_num = len(env_fns) + self.worker_class = self.workers[0].__class__ + + self.wait_num = wait_num or len(env_fns) + assert 1 <= self.wait_num <= len(env_fns), \ + f'wait_num should be in [1, {len(env_fns)}], but got {wait_num}' + self.is_async = self.wait_num != self.env_num + self.waiting_conn = [] + # environments in self.ready_id is actually ready + # but environments in self.waiting_id are just waiting when checked, + # and they may be ready now, but this is not known until we check it + # in the step() function + self.waiting_id = [] + # all environments are ready in the beginning + self.ready_id = list(range(self.env_num)) + + def __len__(self) -> int: + """Return len(self), which is the number of environments.""" + return self.env_num + + def __getattribute__(self, key: str): + """Switch between the default attribute getter or one + looking at wrapped environment level depending on the key.""" + if key not in ('observation_space', 'action_space'): + return super().__getattribute__(key) + else: + return self.__getattr__(key) + + def __getattr__(self, key: str): + """Try to retrieve an attribute from each individual wrapped + environment, if it does not belong to the wrapping vector + environment class.""" + return [getattr(worker, key) for worker in self.workers] + + def _assert_and_transform_id(self, + id: Optional[Union[int, List[int]]] = None + ) -> List[int]: + if id is None: + id = list(range(self.env_num)) + elif np.isscalar(id): + id = [id] + for i in id: + assert i not in self.waiting_id, \ + f'Cannot manipulate environment {i} which is stepping now!' + assert i in self.ready_id, \ + f'Can only manipulate ready environments {self.ready_id}.' + return id + + def reset(self, id: Optional[Union[int, List[int]]] = None): + """Reset the state of all the environments and return initial + observations if id is ``None``, otherwise reset the specific + environments with given id, either an int or a list. + """ + if id is None: + id = range(self.env_num) + elif np.isscalar(id): + id = [id] + if self.is_async: + id = self._assert_and_transform_id(id) + obs = np.stack([self.workers[i].reset() for i in id]) + return obs + + def step(self, + action: Optional[np.ndarray], + id: Optional[Union[int, List[int]]] = None + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Run one timestep of all the environments’ dynamics if id is + ``None``, otherwise run one timestep for some environments + with given id, either an int or a list. When the end of + episode is reached, you are responsible for calling reset(id) + to reset this environment’s state. + + Accept a batch of action and return a tuple (obs, rew, done, info). + + :param numpy.ndarray action: a batch of action provided by the agent. + + :return: A tuple including four items: + + * ``obs`` a numpy.ndarray, the agent's observation of current \ + environments + * ``rew`` a numpy.ndarray, the amount of rewards returned after \ + previous actions + * ``done`` a numpy.ndarray, whether these episodes have ended, in \ + which case further step() calls will return undefined results + * ``info`` a numpy.ndarray, contains auxiliary diagnostic \ + information (helpful for debugging, and sometimes learning) + + For the async simulation: + + Provide the given action to the environments. The action sequence + should correspond to the ``id`` argument, and the ``id`` argument + should be a subset of the ``env_id`` in the last returned ``info`` + (initially they are env_ids of all the environments). If action is + ``None``, fetch unfinished step() calls instead. + """ + if not self.is_async: + if id is None: + id = range(self.env_num) + elif np.isscalar(id): + id = [id] + assert len(action) == len(id) + result = [self.workers[j].step(action[i]) for + i, j in enumerate(id)] + obs, rew, done, info = map(np.stack, zip(*result)) + return obs, rew, done, info + else: + if action is not None: + id = self._assert_and_transform_id(id) + assert len(action) == len(id) + for i, (act, env_id) in enumerate(zip(action, id)): + self.workers[env_id].send_action(act) + self.waiting_conn.append(self.workers[env_id]) + self.waiting_id.append(env_id) + self.ready_id = [x for x in self.ready_id if x not in id] + result = [] + while len(self.waiting_conn) > 0 and len(result) < self.wait_num: + ready_conns = self.worker_class.wait(self.waiting_conn) + for conn in ready_conns: + waiting_index = self.waiting_conn.index(conn) + self.waiting_conn.pop(waiting_index) + env_id = self.waiting_id.pop(waiting_index) + ans = conn.get_result() + obs, rew, done, info = ans + info["env_id"] = env_id + result.append((obs, rew, done, info)) + self.ready_id.append(env_id) + obs, rew, done, info = map(np.stack, zip(*result)) + return obs, rew, done, info + + def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: + """Set the seed for all environments. + + Accept ``None``, an int (which will extend ``i`` to + ``[i, i + 1, i + 2, ...]``) or a list. + + :return: The list of seeds used in this env's random number \ + generators. The first value in the list should be the "main" seed, or \ + the value which a reproducer pass to "seed". + """ + if np.isscalar(seed): + seed = [seed + _ for _ in range(self.env_num)] + elif seed is None: + seed = [seed] * self.env_num + result = [w.seed(s) for w, s in zip(self.workers, seed)] + return result + + def render(self, **kwargs) -> List[Any]: + """Render all of the environments.""" + if self.is_async and len(self.waiting_id) > 0: + raise RuntimeError( + f"Environments {self.waiting_id} are still " + f"stepping, cannot render them now.") + return [w.render(**kwargs) for w in self.workers] + + @run_once + def close(self) -> List[Any]: + """Close all of the environments. This function will be called + only once (if not, it will be called during garbage collected). + This way, ``close`` of all workers can be assured. + """ + if self.is_async: + # finish remaining steps, and close + self.step(None) + return [w.close() for w in self.workers] + + def __del__(self): + """Close the environment before garbage collected""" + try: + self.close() + except RuntimeError: + # it has already been closed + pass + + +class VectorEnv(BaseVectorEnv): + def __init__(self, + env_fns: List[Callable[[], gym.Env]], + wait_num: Optional[int] = None, + ) -> None: + super(VectorEnv, self).__init__( + env_fns, + lambda fn: SequentialEnvWorker(fn), + wait_num=wait_num, + ) + + +class SubprocVectorEnv(BaseVectorEnv): + """Vectorized environment wrapper based on subprocess. + + .. seealso:: + + Please refer to :class:`~tianshou.env.BaseVectorEnv` for more detailed + explanation. + """ + + def __init__(self, + env_fns: List[Callable[[], gym.Env]], + wait_num: Optional[int] = None, + ) -> None: + super(SubprocVectorEnv, self).__init__( + env_fns, + lambda fn: SubProcEnvWorker(fn), + wait_num=wait_num, + ) + + +class ShmemVectorEnv(BaseVectorEnv): + """Optimized version of SubprocVectorEnv that uses shared variables to + communicate observations. SubprocVectorEnv has exactly the same API as + SubprocVectorEnv. + + .. seealso:: + + Please refer to :class:`~tianshou.env.SubprocVectorEnv` for more + detailed explanation. + """ + + def __init__(self, + env_fns: List[Callable[[], gym.Env]], + wait_num: Optional[int] = None, + ) -> None: + super(ShmemVectorEnv, self).__init__( + env_fns, + lambda fn: SubProcEnvWorker(fn, share_memory=True), + wait_num=wait_num, + ) + + +class RayVectorEnv(BaseVectorEnv): + """Vectorized environment wrapper based on + `ray `_. This is a choice to run + distributed environments in a cluster. + + .. seealso:: + + Please refer to :class:`~tianshou.env.BaseVectorEnv` for more detailed + explanation. + """ + + def __init__(self, + env_fns: List[Callable[[], gym.Env]], + wait_num: Optional[int] = None, + ) -> None: + try: + import ray + except ImportError as e: + raise ImportError( + 'Please install ray to support RayVectorEnv: pip install ray' + ) from e + + if not ray.is_initialized(): + ray.init() + super().__init__( + env_fns, + lambda fn: RayEnvWorker(fn), + wait_num=wait_num, + ) From 8eda6c8d2d4a67f8e9ce6b94795da9754b1c452f Mon Sep 17 00:00:00 2001 From: youkaichao Date: Tue, 4 Aug 2020 21:54:53 +0800 Subject: [PATCH 04/74] doc fix --- tianshou/env/venvs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 094d67d20..a8eed6aa6 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -45,11 +45,11 @@ def seed(self, seed): :param wait_num: used in asynchronous simulation if the time cost of - ``env.step`` varies with time and synchronously waiting for all - environments to finish a step is time-wasting. In that case, we can - return when ``wait_num`` environments finish a step and keep on - simulation in these environments. If ``None``, asynchronous simulation - is disabled; else, ``1 <= wait_num <= env_num``. + ``env.step`` varies with time and synchronously waiting for all + environments to finish a step is time-wasting. In that case, we can + return when ``wait_num`` environments finish a step and keep on + simulation in these environments. If ``None``, asynchronous simulation + is disabled; else, ``1 <= wait_num <= env_num``. """ From 01628a1654a796483ea70f33006b9433013f9a15 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Tue, 4 Aug 2020 21:58:48 +0800 Subject: [PATCH 05/74] fix ray import --- tianshou/env/venvs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index a8eed6aa6..96d4b95b9 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -4,7 +4,6 @@ from tianshou.env.worker.base import EnvWorker from tianshou.env.worker.subproc import SubProcEnvWorker from tianshou.env.worker.dummy import SequentialEnvWorker -from tianshou.env.worker.ray import RayEnvWorker from tianshou.utils import run_once @@ -312,6 +311,7 @@ def __init__(self, if not ray.is_initialized(): ray.init() + from tianshou.env.worker.ray import RayEnvWorker super().__init__( env_fns, lambda fn: RayEnvWorker(fn), From 87fc0f03cbca2c4144f7cc98d6c279472efd57a9 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Tue, 4 Aug 2020 22:38:00 +0800 Subject: [PATCH 06/74] fix close in profile & make run_once specific to vec env --- test/throughput/test_collector_profile.py | 8 +++---- tianshou/env/venvs.py | 26 ++++++++++++++++++++++- tianshou/utils/__init__.py | 18 ---------------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/test/throughput/test_collector_profile.py b/test/throughput/test_collector_profile.py index 7d24d27fb..33cf67ef8 100644 --- a/test/throughput/test_collector_profile.py +++ b/test/throughput/test_collector_profile.py @@ -86,7 +86,7 @@ def data(): def test_init(data): for _ in range(5000): c = Collector(data["policy"], data["env"], data["buffer"]) - c.close() + c.close() def test_reset(data): @@ -112,7 +112,7 @@ def test_sample(data): def test_init_vec_env(data): for _ in range(5000): c = Collector(data["policy"], data["env_vec"], data["buffer"]) - c.close() + c.close() def test_reset_vec_env(data): @@ -138,9 +138,7 @@ def test_sample_vec_env(data): def test_init_subproc_env(data): for _ in range(5000): c = Collector(data["policy"], data["env_subproc_init"], data["buffer"]) - """TODO: This should be changed to c.close() in theory, - but currently subproc_env doesn't support that.""" - c.reset() + c.close() def test_reset_subproc_env(data): diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 96d4b95b9..d13a7e28c 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -4,7 +4,30 @@ from tianshou.env.worker.base import EnvWorker from tianshou.env.worker.subproc import SubProcEnvWorker from tianshou.env.worker.dummy import SequentialEnvWorker -from tianshou.utils import run_once + + +def run_once(f): + """ + Run once decorator for a method in a class. Each instance can run + the method at most once. + """ + f.has_run_objects = set() + + def wrapper(self, *args, **kwargs): + if self.unique_id in f.has_run_objects: + raise RuntimeError( + f'{f} can be called only once for object {self}') + f.has_run_objects.add(self.unique_id) + return f(self, *args, **kwargs) + return wrapper + + +def generate_id(): + generate_id.i += 1 + return generate_id.i + + +generate_id.i = 0 class BaseVectorEnv(gym.Env): @@ -74,6 +97,7 @@ def __init__(self, self.waiting_id = [] # all environments are ready in the beginning self.ready_id = list(range(self.env_num)) + self.unique_id = generate_id() def __len__(self) -> int: """Return len(self), which is the number of environments.""" diff --git a/tianshou/utils/__init__.py b/tianshou/utils/__init__.py index eabe1190f..95736dabc 100644 --- a/tianshou/utils/__init__.py +++ b/tianshou/utils/__init__.py @@ -1,25 +1,7 @@ from tianshou.utils.config import tqdm_config from tianshou.utils.moving_average import MovAvg - -def run_once(f): - """ - Run once decorator for a method in a class. Each instance can run - the method at most once. - """ - f.has_run_objects = set() - - def wrapper(self, *args, **kwargs): - if hash(self) in f.has_run_objects: - raise RuntimeError( - f'{f} can be called only once for object {self}') - f.has_run_objects.add(hash(self)) - return f(self, *args, **kwargs) - return wrapper - - __all__ = [ 'MovAvg', - 'run_once', 'tqdm_config', ] From 556f094395bf64c5b68c04cd2cfd353350463b4a Mon Sep 17 00:00:00 2001 From: youkaichao Date: Tue, 4 Aug 2020 23:10:06 +0800 Subject: [PATCH 07/74] correctly close and bugfix --- tianshou/env/venvs.py | 5 +++-- tianshou/env/worker/subproc.py | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index d13a7e28c..9e8a89e6d 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -88,7 +88,7 @@ def __init__(self, self.wait_num = wait_num or len(env_fns) assert 1 <= self.wait_num <= len(env_fns), \ f'wait_num should be in [1, {len(env_fns)}], but got {wait_num}' - self.is_async = self.wait_num != self.env_num + self.is_async = not wait_num self.waiting_conn = [] # environments in self.ready_id is actually ready # but environments in self.waiting_id are just waiting when checked, @@ -245,7 +245,8 @@ def close(self) -> List[Any]: """ if self.is_async: # finish remaining steps, and close - self.step(None) + if len(self.waiting_conn) > 0: + self.step(None) return [w.close() for w in self.workers] def __del__(self): diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index 8b0fab4ac..b0869b493 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -26,7 +26,12 @@ def _encode_obs(obs, buffer): env = env_fn_wrapper.data() try: while True: - cmd, data = p.recv() + try: + cmd, data = p.recv() + except EOFError: + # the pipe has been closed + p.close() + break if cmd == 'step': obs, reward, done, info = env.step(data) if obs_bufs is not None: From 5bcc4f4f589b03762168c2c046e69eb525c3054a Mon Sep 17 00:00:00 2001 From: youkaichao Date: Tue, 4 Aug 2020 23:39:57 +0800 Subject: [PATCH 08/74] bugfix for _batch_set_item and is_async --- test/base/test_collector.py | 2 +- tianshou/data/collector.py | 11 ++++++++--- tianshou/env/venvs.py | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/test/base/test_collector.py b/test/base/test_collector.py index 19c724988..5f64166dd 100644 --- a/test/base/test_collector.py +++ b/test/base/test_collector.py @@ -116,7 +116,7 @@ def test_collector_with_async(): env_fns = [lambda x=i: MyTestEnv(size=x, sleep=0.1, random_sleep=True) for i in env_lens] - venv = SubprocVectorEnv(env_fns) + venv = SubprocVectorEnv(env_fns, wait_num=len(env_fns)) policy = MyPolicy() c1 = Collector(policy, venv, ReplayBuffer(size=1000, ignore_obs_next=False), diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index 6d4a11cfa..80094f900 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -371,10 +371,13 @@ def sample(self, batch_size: int) -> Batch: def _batch_set_item(source: Batch, indices: np.ndarray, target: Batch, size: int): - # for any key chain k, there are three cases + # for any key chain k, there are four cases # 1. source[k] is non-reserved, but target[k] does not exist or is reserved # 2. source[k] does not exist or is reserved, but target[k] is non-reserved - # 3. both source[k] and target[k] is non-reserved + # 3. both source[k] and target[k] are non-reserved + # 4. both source[k] and target[k] do not exist or are reserved, do nothing. + # A special case in case 4, if target[k] is reserved but source[k] does + # not exist, make source[k] reserved, too. for k, v in target.items(): if not isinstance(v, Batch) or not v.is_empty(): # target[k] is non-reserved @@ -385,6 +388,8 @@ def _batch_set_item(source: Batch, indices: np.ndarray, source.__dict__[k] = _create_value(v[0], size) else: # target[k] is reserved - # case 1 + # case 1 or special case of case 4 + if k not in source.__dict__: + source.__dict__[k] = Batch() continue source.__dict__[k][indices] = v diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 9e8a89e6d..378d659fd 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -88,7 +88,7 @@ def __init__(self, self.wait_num = wait_num or len(env_fns) assert 1 <= self.wait_num <= len(env_fns), \ f'wait_num should be in [1, {len(env_fns)}], but got {wait_num}' - self.is_async = not wait_num + self.is_async = wait_num is not None self.waiting_conn = [] # environments in self.ready_id is actually ready # but environments in self.waiting_id are just waiting when checked, From 4706fb04632556f15afaab9be7a5f88159afe526 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Tue, 4 Aug 2020 23:44:04 +0800 Subject: [PATCH 09/74] bugfix for incorrectly re-used collector in test_sac_with_il.py --- test/continuous/test_sac_with_il.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/continuous/test_sac_with_il.py b/test/continuous/test_sac_with_il.py index 96e034054..31728f12a 100644 --- a/test/continuous/test_sac_with_il.py +++ b/test/continuous/test_sac_with_il.py @@ -123,7 +123,10 @@ def stop_fn(x): ).to(args.device) optim = torch.optim.Adam(net.parameters(), lr=args.il_lr) il_policy = ImitationPolicy(net, optim, mode='continuous') - il_test_collector = Collector(il_policy, test_envs) + il_test_collector = Collector( + il_policy, + VectorEnv([lambda: gym.make(args.task) for _ in range(args.test_num)]) + ) train_collector.reset() result = offpolicy_trainer( il_policy, train_collector, il_test_collector, args.epoch, From 836325d2957df94bb67681fc96f4df2130fb433c Mon Sep 17 00:00:00 2001 From: youkaichao Date: Tue, 4 Aug 2020 23:55:48 +0800 Subject: [PATCH 10/74] bugfix for incorrectly re-used collector in test/discrete/test_a2c_with_il.py --- test/discrete/test_a2c_with_il.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/discrete/test_a2c_with_il.py b/test/discrete/test_a2c_with_il.py index 365fb1234..3c7552c59 100644 --- a/test/discrete/test_a2c_with_il.py +++ b/test/discrete/test_a2c_with_il.py @@ -111,7 +111,10 @@ def stop_fn(x): net = Actor(net, args.action_shape).to(args.device) optim = torch.optim.Adam(net.parameters(), lr=args.il_lr) il_policy = ImitationPolicy(net, optim, mode='discrete') - il_test_collector = Collector(il_policy, test_envs) + il_test_collector = Collector( + il_policy, + VectorEnv([lambda: gym.make(args.task) for _ in range(args.test_num)]) + ) train_collector.reset() result = offpolicy_trainer( il_policy, train_collector, il_test_collector, args.epoch, From 8bd0d7428be7b93b5e3ff2d3555d8b81108bd001 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Wed, 5 Aug 2020 08:50:57 +0800 Subject: [PATCH 11/74] improve for send_action and get_result --- tianshou/env/worker/base.py | 7 ++++--- tianshou/env/worker/dummy.py | 5 ++--- tianshou/env/worker/ray.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 18532e77a..76d33f86e 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -10,6 +10,7 @@ class EnvWorker(ABC, gym.Env): def __init__(self, env_fn: Callable[[], gym.Env]) -> None: self._env_fn = env_fn + self._result = None def __getattribute__(self, key: str): if key not in ('observation_space', 'action_space'): @@ -25,13 +26,13 @@ def __getattr__(self, key: str): def reset(self): pass + @abstractmethod def send_action(self, action: np.ndarray): - self.action = action + pass - @abstractmethod def get_result(self ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - pass + return self._result def step(self, action: np.ndarray ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index 29c676bb9..e4e93c0ce 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -29,9 +29,8 @@ def __getattr__(self, key: str): def reset(self): return self.env.reset() - def get_result(self - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - return self.env.step(self.action) + def send_action(self, action: np.ndarray): + self._result = self.env.step(self.action) def seed(self, seed: Optional[int] = None): return self.env.seed(seed) if hasattr(self.env, 'seed') else None diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index cdbc7b5c6..fccd45f4a 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -28,11 +28,11 @@ def reset(self): def get_result(self ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - return ray.get(self.action) + return ray.get(self._result) def send_action(self, action: np.ndarray): # self.action is actually a handle - self.action = self.env.step.remote(action) + self._result = self.env.step.remote(action) def seed(self, seed: Optional[int] = None): if hasattr(self.env, 'seed'): From 7a5322d5b50aecc60d6ebd561e28bd4cba6fb3b9 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Wed, 5 Aug 2020 09:24:33 +0800 Subject: [PATCH 12/74] rename VectorEnv to ForLoopVectorEnv and bugfix --- README.md | 4 ++-- examples/acrobot_dualdqn.py | 6 +++--- examples/ant_v2_ddpg.py | 4 ++-- examples/ant_v2_sac.py | 4 ++-- examples/ant_v2_td3.py | 4 ++-- examples/point_maze_td3.py | 4 ++-- examples/sac_mcc.py | 6 +++--- test/base/test_collector.py | 8 ++++---- test/base/test_env.py | 4 ++-- test/continuous/test_ddpg.py | 6 +++--- test/continuous/test_ppo.py | 6 +++--- test/continuous/test_sac_with_il.py | 8 ++++---- test/continuous/test_td3.py | 6 +++--- test/discrete/test_a2c_with_il.py | 8 ++++---- test/discrete/test_dqn.py | 6 +++--- test/discrete/test_drqn.py | 6 +++--- test/discrete/test_pdqn.py | 6 +++--- test/discrete/test_pg.py | 6 +++--- test/discrete/test_ppo.py | 6 +++--- test/multiagent/Gomoku.py | 4 ++-- test/multiagent/tic_tac_toe.py | 6 +++--- test/throughput/test_collector_profile.py | 4 ++-- tianshou/data/collector.py | 4 ++-- tianshou/env/__init__.py | 4 +++- tianshou/env/venvs.py | 16 ++++++++++++++++ tianshou/env/worker/base.py | 3 +-- tianshou/env/worker/dummy.py | 2 +- tianshou/env/worker/ray.py | 4 ++-- tianshou/env/worker/subproc.py | 7 +++++-- 29 files changed, 91 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 414e288c3..6b9987963 100644 --- a/README.md +++ b/README.md @@ -199,8 +199,8 @@ Make environments: ```python # you can also try with SubprocVectorEnv -train_envs = ts.env.VectorEnv([lambda: gym.make(task) for _ in range(train_num)]) -test_envs = ts.env.VectorEnv([lambda: gym.make(task) for _ in range(test_num)]) +train_envs = ts.env.ForLoopVectorEnv([lambda: gym.make(task) for _ in range(train_num)]) +test_envs = ts.env.ForLoopVectorEnv([lambda: gym.make(task) for _ in range(test_num)]) ``` Define the network: diff --git a/examples/acrobot_dualdqn.py b/examples/acrobot_dualdqn.py index a5b0ac29c..9aa23729d 100644 --- a/examples/acrobot_dualdqn.py +++ b/examples/acrobot_dualdqn.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.policy import DQNPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -46,10 +46,10 @@ def test_dqn(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/examples/ant_v2_ddpg.py b/examples/ant_v2_ddpg.py index 9c35ccfdc..3a257190b 100644 --- a/examples/ant_v2_ddpg.py +++ b/examples/ant_v2_ddpg.py @@ -8,7 +8,7 @@ from tianshou.policy import DDPGPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import VectorEnv, SubprocVectorEnv +from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv from tianshou.exploration import GaussianNoise from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import Actor, Critic @@ -46,7 +46,7 @@ def test_ddpg(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( diff --git a/examples/ant_v2_sac.py b/examples/ant_v2_sac.py index cdfc8138f..b9bd297e1 100644 --- a/examples/ant_v2_sac.py +++ b/examples/ant_v2_sac.py @@ -9,7 +9,7 @@ from tianshou.policy import SACPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import VectorEnv, SubprocVectorEnv +from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import ActorProb, Critic @@ -47,7 +47,7 @@ def test_sac(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( diff --git a/examples/ant_v2_td3.py b/examples/ant_v2_td3.py index 55a45a402..e2d8102f1 100644 --- a/examples/ant_v2_td3.py +++ b/examples/ant_v2_td3.py @@ -8,7 +8,7 @@ from tianshou.policy import TD3Policy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import VectorEnv, SubprocVectorEnv +from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv from tianshou.exploration import GaussianNoise from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import Actor, Critic @@ -49,7 +49,7 @@ def test_td3(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( diff --git a/examples/point_maze_td3.py b/examples/point_maze_td3.py index 0a79c2c5c..d842a2d07 100644 --- a/examples/point_maze_td3.py +++ b/examples/point_maze_td3.py @@ -8,7 +8,7 @@ from tianshou.policy import TD3Policy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import VectorEnv, SubprocVectorEnv +from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv from tianshou.exploration import GaussianNoise from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import Actor, Critic @@ -53,7 +53,7 @@ def test_td3(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( diff --git a/examples/sac_mcc.py b/examples/sac_mcc.py index fcd2ce447..e8b2f4a36 100644 --- a/examples/sac_mcc.py +++ b/examples/sac_mcc.py @@ -9,7 +9,7 @@ from tianshou.policy import SACPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.exploration import OUNoise from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import ActorProb, Critic @@ -51,10 +51,10 @@ def test_sac(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/base/test_collector.py b/test/base/test_collector.py index 5f64166dd..41f120447 100644 --- a/test/base/test_collector.py +++ b/test/base/test_collector.py @@ -2,7 +2,7 @@ from torch.utils.tensorboard import SummaryWriter from tianshou.policy import BasePolicy -from tianshou.env import VectorEnv, SubprocVectorEnv +from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv from tianshou.data import Collector, Batch, ReplayBuffer if __name__ == '__main__': @@ -66,7 +66,7 @@ def test_collector(): env_fns = [lambda x=i: MyTestEnv(size=x, sleep=0) for i in [2, 3, 4, 5]] venv = SubprocVectorEnv(env_fns) - dum = VectorEnv(env_fns) + dum = ForLoopVectorEnv(env_fns) policy = MyPolicy() env = env_fns[0]() c0 = Collector(policy, env, ReplayBuffer(size=100, ignore_obs_next=False), @@ -165,7 +165,7 @@ def test_collector_with_dict_state(): c0.collect(n_episode=2) env_fns = [lambda x=i: MyTestEnv(size=x, sleep=0, dict_state=True) for i in [2, 3, 4, 5]] - envs = VectorEnv(env_fns) + envs = ForLoopVectorEnv(env_fns) envs.seed(666) obs = envs.reset() assert not np.isclose(obs[0]['rand'], obs[1]['rand']) @@ -202,7 +202,7 @@ def reward_metric(x): assert np.asanyarray(r).size == 1 and r == 4. env_fns = [lambda x=i: MyTestEnv(size=x, sleep=0, ma_rew=4) for i in [2, 3, 4, 5]] - envs = VectorEnv(env_fns) + envs = ForLoopVectorEnv(env_fns) c1 = Collector(policy, envs, ReplayBuffer(size=100), Logger.single_preprocess_fn, reward_metric=reward_metric) r = c1.collect(n_step=10)['rew'] diff --git a/test/base/test_env.py b/test/base/test_env.py index 6e37c638c..77bccbb4c 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -2,7 +2,7 @@ import numpy as np from gym.spaces.discrete import Discrete from tianshou.data import Batch -from tianshou.env import VectorEnv, SubprocVectorEnv, \ +from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv, \ RayVectorEnv, ShmemVectorEnv if __name__ == '__main__': @@ -78,7 +78,7 @@ def test_vecenv(size=10, num=8, sleep=0.001): for i in range(size, size + num) ] venv = [ - VectorEnv(env_fns), + ForLoopVectorEnv(env_fns), SubprocVectorEnv(env_fns), ShmemVectorEnv(env_fns), ] diff --git a/test/continuous/test_ddpg.py b/test/continuous/test_ddpg.py index 6f078bcaa..471153cab 100644 --- a/test/continuous/test_ddpg.py +++ b/test/continuous/test_ddpg.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.policy import DDPGPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -55,10 +55,10 @@ def test_ddpg(args=get_args()): args.max_action = env.action_space.high[0] # you can also use tianshou.env.SubprocVectorEnv # train_envs = gym.make(args.task) - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/continuous/test_ppo.py b/test/continuous/test_ppo.py index 73aad3446..0bbb5186e 100644 --- a/test/continuous/test_ppo.py +++ b/test/continuous/test_ppo.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.policy import PPOPolicy from tianshou.policy.dist import DiagGaussian from tianshou.trainer import onpolicy_trainer @@ -58,10 +58,10 @@ def test_ppo(args=get_args()): args.max_action = env.action_space.high[0] # you can also use tianshou.env.SubprocVectorEnv # train_envs = gym.make(args.task) - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/continuous/test_sac_with_il.py b/test/continuous/test_sac_with_il.py index 31728f12a..50ee89e3a 100644 --- a/test/continuous/test_sac_with_il.py +++ b/test/continuous/test_sac_with_il.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer from tianshou.policy import SACPolicy, ImitationPolicy @@ -54,10 +54,10 @@ def test_sac_with_il(args=get_args()): args.max_action = env.action_space.high[0] # you can also use tianshou.env.SubprocVectorEnv # train_envs = gym.make(args.task) - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) @@ -125,7 +125,7 @@ def stop_fn(x): il_policy = ImitationPolicy(net, optim, mode='continuous') il_test_collector = Collector( il_policy, - VectorEnv([lambda: gym.make(args.task) for _ in range(args.test_num)]) + ForLoopVectorEnv([lambda: gym.make(args.task) for _ in range(args.test_num)]) ) train_collector.reset() result = offpolicy_trainer( diff --git a/test/continuous/test_td3.py b/test/continuous/test_td3.py index 096290b6b..b3df682a3 100644 --- a/test/continuous/test_td3.py +++ b/test/continuous/test_td3.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.policy import TD3Policy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -57,10 +57,10 @@ def test_td3(args=get_args()): args.max_action = env.action_space.high[0] # you can also use tianshou.env.SubprocVectorEnv # train_envs = gym.make(args.task) - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/discrete/test_a2c_with_il.py b/test/discrete/test_a2c_with_il.py index 3c7552c59..bf8f5721b 100644 --- a/test/discrete/test_a2c_with_il.py +++ b/test/discrete/test_a2c_with_il.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.data import Collector, ReplayBuffer from tianshou.policy import A2CPolicy, ImitationPolicy from tianshou.trainer import onpolicy_trainer, offpolicy_trainer @@ -52,10 +52,10 @@ def test_a2c_with_il(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # you can also use tianshou.env.SubprocVectorEnv # train_envs = gym.make(args.task) - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) @@ -113,7 +113,7 @@ def stop_fn(x): il_policy = ImitationPolicy(net, optim, mode='discrete') il_test_collector = Collector( il_policy, - VectorEnv([lambda: gym.make(args.task) for _ in range(args.test_num)]) + ForLoopVectorEnv([lambda: gym.make(args.task) for _ in range(args.test_num)]) ) train_collector.reset() result = offpolicy_trainer( diff --git a/test/discrete/test_dqn.py b/test/discrete/test_dqn.py index 0455f7059..ae29d8252 100644 --- a/test/discrete/test_dqn.py +++ b/test/discrete/test_dqn.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.policy import DQNPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -46,10 +46,10 @@ def test_dqn(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/discrete/test_drqn.py b/test/discrete/test_drqn.py index 48573e736..a02c1293e 100644 --- a/test/discrete/test_drqn.py +++ b/test/discrete/test_drqn.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.policy import DQNPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -47,10 +47,10 @@ def test_drqn(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task)for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/discrete/test_pdqn.py b/test/discrete/test_pdqn.py index b614f248a..8bfb9b082 100644 --- a/test/discrete/test_pdqn.py +++ b/test/discrete/test_pdqn.py @@ -7,7 +7,7 @@ from torch.utils.tensorboard import SummaryWriter from tianshou.utils.net.common import Net -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.policy import DQNPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer, PrioritizedReplayBuffer @@ -49,10 +49,10 @@ def test_pdqn(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/discrete/test_pg.py b/test/discrete/test_pg.py index fabfdc9aa..9140b3c60 100644 --- a/test/discrete/test_pg.py +++ b/test/discrete/test_pg.py @@ -8,7 +8,7 @@ from torch.utils.tensorboard import SummaryWriter from tianshou.utils.net.common import Net -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.policy import PGPolicy from tianshou.trainer import onpolicy_trainer from tianshou.data import Batch, Collector, ReplayBuffer @@ -112,10 +112,10 @@ def test_pg(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/discrete/test_ppo.py b/test/discrete/test_ppo.py index ca0e87930..74b811d74 100644 --- a/test/discrete/test_ppo.py +++ b/test/discrete/test_ppo.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.policy import PPOPolicy from tianshou.trainer import onpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -54,10 +54,10 @@ def test_ppo(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = VectorEnv( + train_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = VectorEnv( + test_envs = ForLoopVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/multiagent/Gomoku.py b/test/multiagent/Gomoku.py index 23793914d..68be9210f 100644 --- a/test/multiagent/Gomoku.py +++ b/test/multiagent/Gomoku.py @@ -4,7 +4,7 @@ from copy import deepcopy from torch.utils.tensorboard import SummaryWriter -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.data import Collector from tianshou.policy import RandomPolicy @@ -37,7 +37,7 @@ def gomoku(args=get_args()): def env_func(): return TicTacToeEnv(args.board_size, args.win_size) - test_envs = VectorEnv([env_func for _ in range(args.test_num)]) + test_envs = ForLoopVectorEnv([env_func for _ in range(args.test_num)]) for r in range(args.self_play_round): rews = [] agent_learn.set_eps(0.0) diff --git a/test/multiagent/tic_tac_toe.py b/test/multiagent/tic_tac_toe.py index 00b8f5643..6aa76257e 100644 --- a/test/multiagent/tic_tac_toe.py +++ b/test/multiagent/tic_tac_toe.py @@ -6,7 +6,7 @@ from typing import Optional, Tuple from torch.utils.tensorboard import SummaryWriter -from tianshou.env import VectorEnv +from tianshou.env import ForLoopVectorEnv from tianshou.utils.net.common import Net from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -106,8 +106,8 @@ def train_agent(args: argparse.Namespace = get_args(), ) -> Tuple[dict, BasePolicy]: def env_func(): return TicTacToeEnv(args.board_size, args.win_size) - train_envs = VectorEnv([env_func for _ in range(args.training_num)]) - test_envs = VectorEnv([env_func for _ in range(args.test_num)]) + train_envs = ForLoopVectorEnv([env_func for _ in range(args.training_num)]) + test_envs = ForLoopVectorEnv([env_func for _ in range(args.test_num)]) # seed np.random.seed(args.seed) torch.manual_seed(args.seed) diff --git a/test/throughput/test_collector_profile.py b/test/throughput/test_collector_profile.py index 33cf67ef8..c96b6eb3b 100644 --- a/test/throughput/test_collector_profile.py +++ b/test/throughput/test_collector_profile.py @@ -5,7 +5,7 @@ from gym.utils import seeding from tianshou.data import Batch, Collector, ReplayBuffer -from tianshou.env import VectorEnv, SubprocVectorEnv +from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv from tianshou.policy import BasePolicy @@ -56,7 +56,7 @@ def data(): np.random.seed(0) env = SimpleEnv() env.seed(0) - env_vec = VectorEnv( + env_vec = ForLoopVectorEnv( [lambda: SimpleEnv() for _ in range(100)]) env_vec.seed(np.random.randint(1000, size=100).tolist()) env_subproc = SubprocVectorEnv( diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index 80094f900..8faaaa54a 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -5,7 +5,7 @@ import numpy as np from typing import Any, Dict, List, Union, Optional, Callable -from tianshou.env import BaseVectorEnv, VectorEnv +from tianshou.env import BaseVectorEnv, ForLoopVectorEnv from tianshou.policy import BasePolicy from tianshou.exploration import BaseNoise from tianshou.data import Batch, ReplayBuffer, ListReplayBuffer, to_numpy @@ -94,7 +94,7 @@ def __init__(self, ) -> None: super().__init__() if not isinstance(env, BaseVectorEnv): - env = VectorEnv([lambda: env]) + env = ForLoopVectorEnv([lambda: env]) self.env = env self.env_num = len(env) # environments that are available in step() diff --git a/tianshou/env/__init__.py b/tianshou/env/__init__.py index 3429a630c..979858e15 100644 --- a/tianshou/env/__init__.py +++ b/tianshou/env/__init__.py @@ -1,9 +1,11 @@ from tianshou.env.venvs import \ - (BaseVectorEnv, VectorEnv, SubprocVectorEnv, ShmemVectorEnv, RayVectorEnv) + (BaseVectorEnv, ForLoopVectorEnv, VectorEnv, SubprocVectorEnv, + ShmemVectorEnv, RayVectorEnv) from tianshou.env.maenv import MultiAgentEnv __all__ = [ 'BaseVectorEnv', + 'ForLoopVectorEnv', 'VectorEnv', 'SubprocVectorEnv', 'RayVectorEnv', diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 378d659fd..882eabb6e 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -1,5 +1,6 @@ import gym import numpy as np +import warnings from typing import List, Tuple, Union, Optional, Callable, Any from tianshou.env.worker.base import EnvWorker from tianshou.env.worker.subproc import SubProcEnvWorker @@ -258,11 +259,26 @@ def __del__(self): pass +class ForLoopVectorEnv(BaseVectorEnv): + def __init__(self, + env_fns: List[Callable[[], gym.Env]], + wait_num: Optional[int] = None, + ) -> None: + super(ForLoopVectorEnv, self).__init__( + env_fns, + lambda fn: SequentialEnvWorker(fn), + wait_num=wait_num, + ) + + class VectorEnv(BaseVectorEnv): def __init__(self, env_fns: List[Callable[[], gym.Env]], wait_num: Optional[int] = None, ) -> None: + warnings.warn( + 'VectorEnv is renamed to ForLoopVectorEnv, and will be removed' + ' in 0.3. Use ForLoopVectorEnv instead!', DeprecationWarning) super(VectorEnv, self).__init__( env_fns, lambda fn: SequentialEnvWorker(fn), diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 76d33f86e..ddb32f0a2 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -10,7 +10,6 @@ class EnvWorker(ABC, gym.Env): def __init__(self, env_fn: Callable[[], gym.Env]) -> None: self._env_fn = env_fn - self._result = None def __getattribute__(self, key: str): if key not in ('observation_space', 'action_space'): @@ -32,7 +31,7 @@ def send_action(self, action: np.ndarray): def get_result(self ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - return self._result + return self.result def step(self, action: np.ndarray ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index e4e93c0ce..c6748d497 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -30,7 +30,7 @@ def reset(self): return self.env.reset() def send_action(self, action: np.ndarray): - self._result = self.env.step(self.action) + self.result = self.env.step(action) def seed(self, seed: Optional[int] = None): return self.env.seed(seed) if hasattr(self.env, 'seed') else None diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index fccd45f4a..5a9fad37c 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -28,11 +28,11 @@ def reset(self): def get_result(self ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - return ray.get(self._result) + return ray.get(self.result) def send_action(self, action: np.ndarray): # self.action is actually a handle - self._result = self.env.step.remote(action) + self.result = self.env.step.remote(action) def seed(self, seed: Optional[int] = None): if hasattr(self.env, 'seed'): diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index b0869b493..c57cc7532 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -143,8 +143,11 @@ def render(self, **kwargs) -> None: return self.parent_remote.recv() def close(self) -> Any: - self.parent_remote.send(['close', None]) - result = self.parent_remote.recv() + try: + self.parent_remote.send(['close', None]) + result = self.parent_remote.recv() + except (BrokenPipeError, EOFError): + result = None self.process.join() return result From 4c383193cc9ff4eba53b7e604448435313e84753 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Wed, 5 Aug 2020 09:26:37 +0800 Subject: [PATCH 13/74] bugfix for sync simulation --- tianshou/env/venvs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 882eabb6e..53885dd11 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -185,8 +185,9 @@ def step(self, elif np.isscalar(id): id = [id] assert len(action) == len(id) - result = [self.workers[j].step(action[i]) for - i, j in enumerate(id)] + for i, j in enumerate(id): + self.workers[j].send_action(action[i]) + result = [self.workers[j].get_result() for j in id] obs, rew, done, info = map(np.stack, zip(*result)) return obs, rew, done, info else: From dd98f061fa5150d68da77e9bbd101ba52aac4f32 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Wed, 5 Aug 2020 09:31:59 +0800 Subject: [PATCH 14/74] pep8 fix --- test/continuous/test_sac_with_il.py | 3 ++- test/discrete/test_a2c_with_il.py | 3 ++- tianshou/env/worker/dummy.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/continuous/test_sac_with_il.py b/test/continuous/test_sac_with_il.py index 50ee89e3a..6d4f94d93 100644 --- a/test/continuous/test_sac_with_il.py +++ b/test/continuous/test_sac_with_il.py @@ -125,7 +125,8 @@ def stop_fn(x): il_policy = ImitationPolicy(net, optim, mode='continuous') il_test_collector = Collector( il_policy, - ForLoopVectorEnv([lambda: gym.make(args.task) for _ in range(args.test_num)]) + ForLoopVectorEnv( + [lambda: gym.make(args.task) for _ in range(args.test_num)]) ) train_collector.reset() result = offpolicy_trainer( diff --git a/test/discrete/test_a2c_with_il.py b/test/discrete/test_a2c_with_il.py index bf8f5721b..e10442776 100644 --- a/test/discrete/test_a2c_with_il.py +++ b/test/discrete/test_a2c_with_il.py @@ -113,7 +113,8 @@ def stop_fn(x): il_policy = ImitationPolicy(net, optim, mode='discrete') il_test_collector = Collector( il_policy, - ForLoopVectorEnv([lambda: gym.make(args.task) for _ in range(args.test_num)]) + ForLoopVectorEnv( + [lambda: gym.make(args.task) for _ in range(args.test_num)]) ) train_collector.reset() result = offpolicy_trainer( diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index c6748d497..023a89dc0 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -1,4 +1,4 @@ -from typing import List, Callable, Tuple, Optional, Any +from typing import List, Callable, Optional, Any import gym import numpy as np From dac13ce6aace2ea484af72f6d4efbff278909592 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Wed, 5 Aug 2020 09:39:20 +0800 Subject: [PATCH 15/74] rename ForLoopVectorEnv to DummyVectorEnv --- README.md | 4 ++-- examples/acrobot_dualdqn.py | 6 +++--- examples/ant_v2_ddpg.py | 4 ++-- examples/ant_v2_sac.py | 4 ++-- examples/ant_v2_td3.py | 4 ++-- examples/point_maze_td3.py | 4 ++-- examples/sac_mcc.py | 6 +++--- test/base/test_collector.py | 8 ++++---- test/base/test_env.py | 4 ++-- test/continuous/test_ddpg.py | 6 +++--- test/continuous/test_ppo.py | 6 +++--- test/continuous/test_sac_with_il.py | 8 ++++---- test/continuous/test_td3.py | 6 +++--- test/discrete/test_a2c_with_il.py | 8 ++++---- test/discrete/test_dqn.py | 6 +++--- test/discrete/test_drqn.py | 6 +++--- test/discrete/test_pdqn.py | 6 +++--- test/discrete/test_pg.py | 6 +++--- test/discrete/test_ppo.py | 6 +++--- test/multiagent/Gomoku.py | 4 ++-- test/multiagent/tic_tac_toe.py | 6 +++--- test/throughput/test_collector_profile.py | 4 ++-- tianshou/data/collector.py | 4 ++-- tianshou/env/__init__.py | 4 ++-- tianshou/env/venvs.py | 14 +++++++------- tianshou/env/worker/dummy.py | 8 ++++---- 26 files changed, 76 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 6b9987963..66ffa8e5c 100644 --- a/README.md +++ b/README.md @@ -199,8 +199,8 @@ Make environments: ```python # you can also try with SubprocVectorEnv -train_envs = ts.env.ForLoopVectorEnv([lambda: gym.make(task) for _ in range(train_num)]) -test_envs = ts.env.ForLoopVectorEnv([lambda: gym.make(task) for _ in range(test_num)]) +train_envs = ts.env.DummyVectorEnv([lambda: gym.make(task) for _ in range(train_num)]) +test_envs = ts.env.DummyVectorEnv([lambda: gym.make(task) for _ in range(test_num)]) ``` Define the network: diff --git a/examples/acrobot_dualdqn.py b/examples/acrobot_dualdqn.py index 9aa23729d..f8896c165 100644 --- a/examples/acrobot_dualdqn.py +++ b/examples/acrobot_dualdqn.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.policy import DQNPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -46,10 +46,10 @@ def test_dqn(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/examples/ant_v2_ddpg.py b/examples/ant_v2_ddpg.py index 3a257190b..0f3b14901 100644 --- a/examples/ant_v2_ddpg.py +++ b/examples/ant_v2_ddpg.py @@ -8,7 +8,7 @@ from tianshou.policy import DDPGPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv +from tianshou.env import DummyVectorEnv, SubprocVectorEnv from tianshou.exploration import GaussianNoise from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import Actor, Critic @@ -46,7 +46,7 @@ def test_ddpg(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( diff --git a/examples/ant_v2_sac.py b/examples/ant_v2_sac.py index b9bd297e1..564415023 100644 --- a/examples/ant_v2_sac.py +++ b/examples/ant_v2_sac.py @@ -9,7 +9,7 @@ from tianshou.policy import SACPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv +from tianshou.env import DummyVectorEnv, SubprocVectorEnv from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import ActorProb, Critic @@ -47,7 +47,7 @@ def test_sac(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( diff --git a/examples/ant_v2_td3.py b/examples/ant_v2_td3.py index e2d8102f1..54a2bb8de 100644 --- a/examples/ant_v2_td3.py +++ b/examples/ant_v2_td3.py @@ -8,7 +8,7 @@ from tianshou.policy import TD3Policy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv +from tianshou.env import DummyVectorEnv, SubprocVectorEnv from tianshou.exploration import GaussianNoise from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import Actor, Critic @@ -49,7 +49,7 @@ def test_td3(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( diff --git a/examples/point_maze_td3.py b/examples/point_maze_td3.py index d842a2d07..86bcdf635 100644 --- a/examples/point_maze_td3.py +++ b/examples/point_maze_td3.py @@ -8,7 +8,7 @@ from tianshou.policy import TD3Policy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv +from tianshou.env import DummyVectorEnv, SubprocVectorEnv from tianshou.exploration import GaussianNoise from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import Actor, Critic @@ -53,7 +53,7 @@ def test_td3(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( diff --git a/examples/sac_mcc.py b/examples/sac_mcc.py index e8b2f4a36..8412f58fd 100644 --- a/examples/sac_mcc.py +++ b/examples/sac_mcc.py @@ -9,7 +9,7 @@ from tianshou.policy import SACPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.exploration import OUNoise from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import ActorProb, Critic @@ -51,10 +51,10 @@ def test_sac(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/base/test_collector.py b/test/base/test_collector.py index 41f120447..3e4d4004a 100644 --- a/test/base/test_collector.py +++ b/test/base/test_collector.py @@ -2,7 +2,7 @@ from torch.utils.tensorboard import SummaryWriter from tianshou.policy import BasePolicy -from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv +from tianshou.env import DummyVectorEnv, SubprocVectorEnv from tianshou.data import Collector, Batch, ReplayBuffer if __name__ == '__main__': @@ -66,7 +66,7 @@ def test_collector(): env_fns = [lambda x=i: MyTestEnv(size=x, sleep=0) for i in [2, 3, 4, 5]] venv = SubprocVectorEnv(env_fns) - dum = ForLoopVectorEnv(env_fns) + dum = DummyVectorEnv(env_fns) policy = MyPolicy() env = env_fns[0]() c0 = Collector(policy, env, ReplayBuffer(size=100, ignore_obs_next=False), @@ -165,7 +165,7 @@ def test_collector_with_dict_state(): c0.collect(n_episode=2) env_fns = [lambda x=i: MyTestEnv(size=x, sleep=0, dict_state=True) for i in [2, 3, 4, 5]] - envs = ForLoopVectorEnv(env_fns) + envs = DummyVectorEnv(env_fns) envs.seed(666) obs = envs.reset() assert not np.isclose(obs[0]['rand'], obs[1]['rand']) @@ -202,7 +202,7 @@ def reward_metric(x): assert np.asanyarray(r).size == 1 and r == 4. env_fns = [lambda x=i: MyTestEnv(size=x, sleep=0, ma_rew=4) for i in [2, 3, 4, 5]] - envs = ForLoopVectorEnv(env_fns) + envs = DummyVectorEnv(env_fns) c1 = Collector(policy, envs, ReplayBuffer(size=100), Logger.single_preprocess_fn, reward_metric=reward_metric) r = c1.collect(n_step=10)['rew'] diff --git a/test/base/test_env.py b/test/base/test_env.py index 77bccbb4c..d74c82f02 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -2,7 +2,7 @@ import numpy as np from gym.spaces.discrete import Discrete from tianshou.data import Batch -from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv, \ +from tianshou.env import DummyVectorEnv, SubprocVectorEnv, \ RayVectorEnv, ShmemVectorEnv if __name__ == '__main__': @@ -78,7 +78,7 @@ def test_vecenv(size=10, num=8, sleep=0.001): for i in range(size, size + num) ] venv = [ - ForLoopVectorEnv(env_fns), + DummyVectorEnv(env_fns), SubprocVectorEnv(env_fns), ShmemVectorEnv(env_fns), ] diff --git a/test/continuous/test_ddpg.py b/test/continuous/test_ddpg.py index 471153cab..0bbc697bf 100644 --- a/test/continuous/test_ddpg.py +++ b/test/continuous/test_ddpg.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.policy import DDPGPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -55,10 +55,10 @@ def test_ddpg(args=get_args()): args.max_action = env.action_space.high[0] # you can also use tianshou.env.SubprocVectorEnv # train_envs = gym.make(args.task) - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/continuous/test_ppo.py b/test/continuous/test_ppo.py index 0bbb5186e..67c80c3c9 100644 --- a/test/continuous/test_ppo.py +++ b/test/continuous/test_ppo.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.policy import PPOPolicy from tianshou.policy.dist import DiagGaussian from tianshou.trainer import onpolicy_trainer @@ -58,10 +58,10 @@ def test_ppo(args=get_args()): args.max_action = env.action_space.high[0] # you can also use tianshou.env.SubprocVectorEnv # train_envs = gym.make(args.task) - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/continuous/test_sac_with_il.py b/test/continuous/test_sac_with_il.py index 6d4f94d93..9bc8ac543 100644 --- a/test/continuous/test_sac_with_il.py +++ b/test/continuous/test_sac_with_il.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer from tianshou.policy import SACPolicy, ImitationPolicy @@ -54,10 +54,10 @@ def test_sac_with_il(args=get_args()): args.max_action = env.action_space.high[0] # you can also use tianshou.env.SubprocVectorEnv # train_envs = gym.make(args.task) - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) @@ -125,7 +125,7 @@ def stop_fn(x): il_policy = ImitationPolicy(net, optim, mode='continuous') il_test_collector = Collector( il_policy, - ForLoopVectorEnv( + DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) ) train_collector.reset() diff --git a/test/continuous/test_td3.py b/test/continuous/test_td3.py index b3df682a3..2d036e04e 100644 --- a/test/continuous/test_td3.py +++ b/test/continuous/test_td3.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.policy import TD3Policy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -57,10 +57,10 @@ def test_td3(args=get_args()): args.max_action = env.action_space.high[0] # you can also use tianshou.env.SubprocVectorEnv # train_envs = gym.make(args.task) - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/discrete/test_a2c_with_il.py b/test/discrete/test_a2c_with_il.py index e10442776..1ea935680 100644 --- a/test/discrete/test_a2c_with_il.py +++ b/test/discrete/test_a2c_with_il.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.data import Collector, ReplayBuffer from tianshou.policy import A2CPolicy, ImitationPolicy from tianshou.trainer import onpolicy_trainer, offpolicy_trainer @@ -52,10 +52,10 @@ def test_a2c_with_il(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # you can also use tianshou.env.SubprocVectorEnv # train_envs = gym.make(args.task) - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) @@ -113,7 +113,7 @@ def stop_fn(x): il_policy = ImitationPolicy(net, optim, mode='discrete') il_test_collector = Collector( il_policy, - ForLoopVectorEnv( + DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) ) train_collector.reset() diff --git a/test/discrete/test_dqn.py b/test/discrete/test_dqn.py index ae29d8252..642d535c2 100644 --- a/test/discrete/test_dqn.py +++ b/test/discrete/test_dqn.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.policy import DQNPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -46,10 +46,10 @@ def test_dqn(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/discrete/test_drqn.py b/test/discrete/test_drqn.py index a02c1293e..f4eb8326e 100644 --- a/test/discrete/test_drqn.py +++ b/test/discrete/test_drqn.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.policy import DQNPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -47,10 +47,10 @@ def test_drqn(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task)for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/discrete/test_pdqn.py b/test/discrete/test_pdqn.py index 8bfb9b082..16c75719a 100644 --- a/test/discrete/test_pdqn.py +++ b/test/discrete/test_pdqn.py @@ -7,7 +7,7 @@ from torch.utils.tensorboard import SummaryWriter from tianshou.utils.net.common import Net -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.policy import DQNPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer, PrioritizedReplayBuffer @@ -49,10 +49,10 @@ def test_pdqn(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/discrete/test_pg.py b/test/discrete/test_pg.py index 9140b3c60..0bf4a8347 100644 --- a/test/discrete/test_pg.py +++ b/test/discrete/test_pg.py @@ -8,7 +8,7 @@ from torch.utils.tensorboard import SummaryWriter from tianshou.utils.net.common import Net -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.policy import PGPolicy from tianshou.trainer import onpolicy_trainer from tianshou.data import Batch, Collector, ReplayBuffer @@ -112,10 +112,10 @@ def test_pg(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/discrete/test_ppo.py b/test/discrete/test_ppo.py index 74b811d74..b7b308c8d 100644 --- a/test/discrete/test_ppo.py +++ b/test/discrete/test_ppo.py @@ -6,7 +6,7 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.policy import PPOPolicy from tianshou.trainer import onpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -54,10 +54,10 @@ def test_ppo(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv - train_envs = ForLoopVectorEnv( + train_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) - test_envs = ForLoopVectorEnv( + test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) # seed np.random.seed(args.seed) diff --git a/test/multiagent/Gomoku.py b/test/multiagent/Gomoku.py index 68be9210f..53652a2e4 100644 --- a/test/multiagent/Gomoku.py +++ b/test/multiagent/Gomoku.py @@ -4,7 +4,7 @@ from copy import deepcopy from torch.utils.tensorboard import SummaryWriter -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.data import Collector from tianshou.policy import RandomPolicy @@ -37,7 +37,7 @@ def gomoku(args=get_args()): def env_func(): return TicTacToeEnv(args.board_size, args.win_size) - test_envs = ForLoopVectorEnv([env_func for _ in range(args.test_num)]) + test_envs = DummyVectorEnv([env_func for _ in range(args.test_num)]) for r in range(args.self_play_round): rews = [] agent_learn.set_eps(0.0) diff --git a/test/multiagent/tic_tac_toe.py b/test/multiagent/tic_tac_toe.py index 6aa76257e..3922d3ebf 100644 --- a/test/multiagent/tic_tac_toe.py +++ b/test/multiagent/tic_tac_toe.py @@ -6,7 +6,7 @@ from typing import Optional, Tuple from torch.utils.tensorboard import SummaryWriter -from tianshou.env import ForLoopVectorEnv +from tianshou.env import DummyVectorEnv from tianshou.utils.net.common import Net from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -106,8 +106,8 @@ def train_agent(args: argparse.Namespace = get_args(), ) -> Tuple[dict, BasePolicy]: def env_func(): return TicTacToeEnv(args.board_size, args.win_size) - train_envs = ForLoopVectorEnv([env_func for _ in range(args.training_num)]) - test_envs = ForLoopVectorEnv([env_func for _ in range(args.test_num)]) + train_envs = DummyVectorEnv([env_func for _ in range(args.training_num)]) + test_envs = DummyVectorEnv([env_func for _ in range(args.test_num)]) # seed np.random.seed(args.seed) torch.manual_seed(args.seed) diff --git a/test/throughput/test_collector_profile.py b/test/throughput/test_collector_profile.py index c96b6eb3b..49ada853e 100644 --- a/test/throughput/test_collector_profile.py +++ b/test/throughput/test_collector_profile.py @@ -5,7 +5,7 @@ from gym.utils import seeding from tianshou.data import Batch, Collector, ReplayBuffer -from tianshou.env import ForLoopVectorEnv, SubprocVectorEnv +from tianshou.env import DummyVectorEnv, SubprocVectorEnv from tianshou.policy import BasePolicy @@ -56,7 +56,7 @@ def data(): np.random.seed(0) env = SimpleEnv() env.seed(0) - env_vec = ForLoopVectorEnv( + env_vec = DummyVectorEnv( [lambda: SimpleEnv() for _ in range(100)]) env_vec.seed(np.random.randint(1000, size=100).tolist()) env_subproc = SubprocVectorEnv( diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index 8faaaa54a..d59e07234 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -5,7 +5,7 @@ import numpy as np from typing import Any, Dict, List, Union, Optional, Callable -from tianshou.env import BaseVectorEnv, ForLoopVectorEnv +from tianshou.env import BaseVectorEnv, DummyVectorEnv from tianshou.policy import BasePolicy from tianshou.exploration import BaseNoise from tianshou.data import Batch, ReplayBuffer, ListReplayBuffer, to_numpy @@ -94,7 +94,7 @@ def __init__(self, ) -> None: super().__init__() if not isinstance(env, BaseVectorEnv): - env = ForLoopVectorEnv([lambda: env]) + env = DummyVectorEnv([lambda: env]) self.env = env self.env_num = len(env) # environments that are available in step() diff --git a/tianshou/env/__init__.py b/tianshou/env/__init__.py index 979858e15..4fefdf08d 100644 --- a/tianshou/env/__init__.py +++ b/tianshou/env/__init__.py @@ -1,11 +1,11 @@ from tianshou.env.venvs import \ - (BaseVectorEnv, ForLoopVectorEnv, VectorEnv, SubprocVectorEnv, + (BaseVectorEnv, DummyVectorEnv, VectorEnv, SubprocVectorEnv, ShmemVectorEnv, RayVectorEnv) from tianshou.env.maenv import MultiAgentEnv __all__ = [ 'BaseVectorEnv', - 'ForLoopVectorEnv', + 'DummyVectorEnv', 'VectorEnv', 'SubprocVectorEnv', 'RayVectorEnv', diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 53885dd11..1fa8a25c8 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -4,7 +4,7 @@ from typing import List, Tuple, Union, Optional, Callable, Any from tianshou.env.worker.base import EnvWorker from tianshou.env.worker.subproc import SubProcEnvWorker -from tianshou.env.worker.dummy import SequentialEnvWorker +from tianshou.env.worker.dummy import DummyEnvWorker def run_once(f): @@ -260,14 +260,14 @@ def __del__(self): pass -class ForLoopVectorEnv(BaseVectorEnv): +class DummyVectorEnv(BaseVectorEnv): def __init__(self, env_fns: List[Callable[[], gym.Env]], wait_num: Optional[int] = None, ) -> None: - super(ForLoopVectorEnv, self).__init__( + super(DummyVectorEnv, self).__init__( env_fns, - lambda fn: SequentialEnvWorker(fn), + lambda fn: DummyEnvWorker(fn), wait_num=wait_num, ) @@ -278,11 +278,11 @@ def __init__(self, wait_num: Optional[int] = None, ) -> None: warnings.warn( - 'VectorEnv is renamed to ForLoopVectorEnv, and will be removed' - ' in 0.3. Use ForLoopVectorEnv instead!', DeprecationWarning) + 'VectorEnv is renamed to DummyVectorEnv, and will be removed' + ' in 0.3. Use DummyVectorEnv instead!', DeprecationWarning) super(VectorEnv, self).__init__( env_fns, - lambda fn: SequentialEnvWorker(fn), + lambda fn: DummyEnvWorker(fn), wait_num=wait_num, ) diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index 023a89dc0..d5ad91c71 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -6,19 +6,19 @@ from tianshou.env.worker.base import EnvWorker -class SequentialEnvWorker(EnvWorker): +class DummyEnvWorker(EnvWorker): """ Dummy worker used in sequential vector environments """ @staticmethod - def wait(workers: List['SequentialEnvWorker'] - ) -> List['SequentialEnvWorker']: + def wait(workers: List['DummyEnvWorker'] + ) -> List['DummyEnvWorker']: # SequentialEnvWorker objects are always ready return workers def __init__(self, env_fn: Callable[[], gym.Env]) -> None: - super(SequentialEnvWorker, self).__init__(env_fn) + super(DummyEnvWorker, self).__init__(env_fn) self.env = env_fn() def __getattr__(self, key: str): From 0e356b56562e288d4933e2dc015779ddaa95ba9d Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 5 Aug 2020 10:34:19 +0800 Subject: [PATCH 16/74] change examples venv --- examples/ant_v2_ddpg.py | 4 ++-- examples/ant_v2_sac.py | 4 ++-- examples/ant_v2_td3.py | 4 ++-- examples/point_maze_td3.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/ant_v2_ddpg.py b/examples/ant_v2_ddpg.py index 0f3b14901..aca2b2932 100644 --- a/examples/ant_v2_ddpg.py +++ b/examples/ant_v2_ddpg.py @@ -8,7 +8,7 @@ from tianshou.policy import DDPGPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import DummyVectorEnv, SubprocVectorEnv +from tianshou.env import SubprocVectorEnv from tianshou.exploration import GaussianNoise from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import Actor, Critic @@ -46,7 +46,7 @@ def test_ddpg(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = DummyVectorEnv( + train_envs = SubprocVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( diff --git a/examples/ant_v2_sac.py b/examples/ant_v2_sac.py index 564415023..d2cf10296 100644 --- a/examples/ant_v2_sac.py +++ b/examples/ant_v2_sac.py @@ -9,7 +9,7 @@ from tianshou.policy import SACPolicy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import DummyVectorEnv, SubprocVectorEnv +from tianshou.env import SubprocVectorEnv from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import ActorProb, Critic @@ -47,7 +47,7 @@ def test_sac(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = DummyVectorEnv( + train_envs = SubprocVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( diff --git a/examples/ant_v2_td3.py b/examples/ant_v2_td3.py index 54a2bb8de..34ce29c5f 100644 --- a/examples/ant_v2_td3.py +++ b/examples/ant_v2_td3.py @@ -8,7 +8,7 @@ from tianshou.policy import TD3Policy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import DummyVectorEnv, SubprocVectorEnv +from tianshou.env import SubprocVectorEnv from tianshou.exploration import GaussianNoise from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import Actor, Critic @@ -49,7 +49,7 @@ def test_td3(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = DummyVectorEnv( + train_envs = SubprocVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( diff --git a/examples/point_maze_td3.py b/examples/point_maze_td3.py index 86bcdf635..bad71daac 100644 --- a/examples/point_maze_td3.py +++ b/examples/point_maze_td3.py @@ -8,7 +8,7 @@ from tianshou.policy import TD3Policy from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.env import DummyVectorEnv, SubprocVectorEnv +from tianshou.env import SubprocVectorEnv from tianshou.exploration import GaussianNoise from tianshou.utils.net.common import Net from tianshou.utils.net.continuous import Actor, Critic @@ -53,7 +53,7 @@ def test_td3(args=get_args()): args.action_shape = env.action_space.shape or env.action_space.n args.max_action = env.action_space.high[0] # train_envs = gym.make(args.task) - train_envs = DummyVectorEnv( + train_envs = SubprocVectorEnv( [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = SubprocVectorEnv( From 74d89613f590c7ea42c98786df0fda83bcc17a04 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 5 Aug 2020 10:37:25 +0800 Subject: [PATCH 17/74] update docs --- docs/tutorials/cheatsheet.rst | 2 +- docs/tutorials/dqn.rst | 6 +++--- docs/tutorials/tictactoe.rst | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/cheatsheet.rst b/docs/tutorials/cheatsheet.rst index 1a715d5de..91e4ddd13 100644 --- a/docs/tutorials/cheatsheet.rst +++ b/docs/tutorials/cheatsheet.rst @@ -31,7 +31,7 @@ See :ref:`customized_trainer`. Parallel Sampling ----------------- -Use :class:`~tianshou.env.VectorEnv`, :class:`~tianshou.env.SubprocVectorEnv` or :class:`~tianshou.env.ShmemVectorEnv`. +Use :class:`~tianshou.env.DummyVectorEnv`, :class:`~tianshou.env.SubprocVectorEnv`, :class:`~tianshou.env.ShmemVectorEnv`, or :class:`~tianshou.env.RayVectorEnv`. :: env_fns = [ diff --git a/docs/tutorials/dqn.rst b/docs/tutorials/dqn.rst index e5edbfd41..209221e7f 100644 --- a/docs/tutorials/dqn.rst +++ b/docs/tutorials/dqn.rst @@ -30,11 +30,11 @@ It is available if you want the original ``gym.Env``: train_envs = gym.make('CartPole-v0') test_envs = gym.make('CartPole-v0') -Tianshou supports parallel sampling for all algorithms. It provides four types of vectorized environment wrapper: :class:`~tianshou.env.VectorEnv`, :class:`~tianshou.env.SubprocVectorEnv`, :class:`~tianshou.env.ShmemVectorEnv`, and :class:`~tianshou.env.RayVectorEnv`. It can be used as follows: +Tianshou supports parallel sampling for all algorithms. It provides four types of vectorized environment wrapper: :class:`~tianshou.env.DummyVectorEnv`, :class:`~tianshou.env.SubprocVectorEnv`, :class:`~tianshou.env.ShmemVectorEnv`, and :class:`~tianshou.env.RayVectorEnv`. It can be used as follows: :: - train_envs = ts.env.VectorEnv([lambda: gym.make('CartPole-v0') for _ in range(8)]) - test_envs = ts.env.VectorEnv([lambda: gym.make('CartPole-v0') for _ in range(100)]) + train_envs = ts.env.DummyVectorEnv([lambda: gym.make('CartPole-v0') for _ in range(8)]) + test_envs = ts.env.DummyVectorEnv([lambda: gym.make('CartPole-v0') for _ in range(100)]) Here, we set up 8 environments in ``train_envs`` and 100 environments in ``test_envs``. diff --git a/docs/tutorials/tictactoe.rst b/docs/tutorials/tictactoe.rst index fe68ccf51..e70901367 100644 --- a/docs/tutorials/tictactoe.rst +++ b/docs/tutorials/tictactoe.rst @@ -175,7 +175,7 @@ So let's start to train our Tic-Tac-Toe agent! First, import some required modul from copy import deepcopy from torch.utils.tensorboard import SummaryWriter - from tianshou.env import VectorEnv + from tianshou.env import DummyVectorEnv from tianshou.utils.net.common import Net from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer @@ -297,8 +297,8 @@ With the above preparation, we are close to the first learned agent. The followi # ======== environment setup ========= env_func = lambda: TicTacToeEnv(args.board_size, args.win_size) - train_envs = VectorEnv([env_func for _ in range(args.training_num)]) - test_envs = VectorEnv([env_func for _ in range(args.test_num)]) + train_envs = DummyVectorEnv([env_func for _ in range(args.training_num)]) + test_envs = DummyVectorEnv([env_func for _ in range(args.test_num)]) # seed np.random.seed(args.seed) torch.manual_seed(args.seed) From abfb3379c62edcc03a846fec35737232d44e31eb Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 5 Aug 2020 10:40:35 +0800 Subject: [PATCH 18/74] update docs --- tianshou/data/collector.py | 3 ++- tianshou/env/venvs.py | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index d59e07234..fbd3d6719 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -51,7 +51,8 @@ class Collector(object): collector = Collector(policy, env, buffer=replay_buffer) # the collector supports vectorized environments as well - envs = VectorEnv([lambda: gym.make('CartPole-v0') for _ in range(3)]) + envs = DummyVectorEnv([lambda: gym.make('CartPole-v0') + for _ in range(3)]) collector = Collector(policy, envs, buffer=replay_buffer) # collect 3 episodes diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 1fa8a25c8..4135ebc47 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -8,9 +8,8 @@ def run_once(f): - """ - Run once decorator for a method in a class. Each instance can run - the method at most once. + """Run once decorator for a method in a class. Each instance can run the + method at most once. """ f.has_run_objects = set() @@ -36,7 +35,7 @@ class BaseVectorEnv(gym.Env): :: env_num = 8 - envs = VectorEnv([lambda: gym.make(task) for _ in range(env_num)]) + envs = DummyVectorEnv([lambda: gym.make(task) for _ in range(env_num)]) assert len(envs) == env_num It accepts a list of environment generators. In other words, an environment From b6b2003ac10ba8c1de89bf883c0bbad18ae6481a Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 5 Aug 2020 20:44:24 +0800 Subject: [PATCH 19/74] remove collector.close --- README.md | 1 - docs/tutorials/dqn.rst | 1 - docs/tutorials/tictactoe.rst | 5 ----- examples/acrobot_dualdqn.py | 3 --- examples/ant_v2_ddpg.py | 3 --- examples/ant_v2_sac.py | 3 --- examples/ant_v2_td3.py | 3 --- examples/bipedal_hardcore_sac.py | 2 -- examples/halfcheetahBullet_v0_sac.py | 3 --- examples/point_maze_td3.py | 3 --- examples/pong_a2c.py | 3 --- examples/pong_dqn.py | 3 --- examples/pong_ppo.py | 3 --- examples/sac_mcc.py | 3 --- test/continuous/test_ddpg.py | 3 --- test/continuous/test_ppo.py | 3 --- test/continuous/test_sac_with_il.py | 5 ----- test/continuous/test_td3.py | 3 --- test/discrete/test_a2c_with_il.py | 5 ----- test/discrete/test_dqn.py | 3 --- test/discrete/test_drqn.py | 3 --- test/discrete/test_pdqn.py | 3 --- test/discrete/test_pg.py | 3 --- test/discrete/test_ppo.py | 3 --- test/multiagent/tic_tac_toe.py | 4 ---- tianshou/data/collector.py | 4 ---- 26 files changed, 81 deletions(-) diff --git a/README.md b/README.md index 66ffa8e5c..0403599fa 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,6 @@ Watch the performance with 35 FPS: ```python collector = ts.data.Collector(policy, env) collector.collect(n_episode=1, render=1 / 35) -collector.close() ``` Look at the result saved in tensorboard: (with bash script in your terminal) diff --git a/docs/tutorials/dqn.rst b/docs/tutorials/dqn.rst index 209221e7f..253021cf9 100644 --- a/docs/tutorials/dqn.rst +++ b/docs/tutorials/dqn.rst @@ -178,7 +178,6 @@ Watch the Agent's Performance collector = ts.data.Collector(policy, env) collector.collect(n_episode=1, render=1 / 35) - collector.close() .. _customized_trainer: diff --git a/docs/tutorials/tictactoe.rst b/docs/tutorials/tictactoe.rst index e70901367..be58771ba 100644 --- a/docs/tutorials/tictactoe.rst +++ b/docs/tutorials/tictactoe.rst @@ -158,7 +158,6 @@ Tianshou already provides some builtin classes for multi-agent learning. You can ===x _ o x _ _=== ===x _ _ _ x x=== ================= - >>> collector.close() Random agents perform badly. In the above game, although agent 2 wins finally, it is clear that a smart agent 1 would place an ``x`` at row 4 col 4 to win directly. @@ -290,7 +289,6 @@ With the above preparation, we are close to the first learned agent. The followi collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if args.watch: watch(args) exit(0) @@ -351,9 +349,6 @@ With the above preparation, we are close to the first learned agent. The followi stop_fn=stop_fn, save_fn=save_fn, writer=writer, test_in_train=False) - train_collector.close() - test_collector.close() - agent = policy.policies[args.agent_id - 1] # let's watch the match! watch(args, agent) diff --git a/examples/acrobot_dualdqn.py b/examples/acrobot_dualdqn.py index f8896c165..9ae679262 100644 --- a/examples/acrobot_dualdqn.py +++ b/examples/acrobot_dualdqn.py @@ -100,8 +100,6 @@ def test_fn(x): stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -109,7 +107,6 @@ def test_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/examples/ant_v2_ddpg.py b/examples/ant_v2_ddpg.py index aca2b2932..3d287c5e7 100644 --- a/examples/ant_v2_ddpg.py +++ b/examples/ant_v2_ddpg.py @@ -86,8 +86,6 @@ def stop_fn(x): args.step_per_epoch, args.collect_per_step, args.test_num, args.batch_size, stop_fn=stop_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -95,7 +93,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/examples/ant_v2_sac.py b/examples/ant_v2_sac.py index d2cf10296..9165b13cc 100644 --- a/examples/ant_v2_sac.py +++ b/examples/ant_v2_sac.py @@ -96,8 +96,6 @@ def stop_fn(x): args.step_per_epoch, args.collect_per_step, args.test_num, args.batch_size, stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -105,7 +103,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/examples/ant_v2_td3.py b/examples/ant_v2_td3.py index 34ce29c5f..665e45874 100644 --- a/examples/ant_v2_td3.py +++ b/examples/ant_v2_td3.py @@ -96,8 +96,6 @@ def stop_fn(x): args.step_per_epoch, args.collect_per_step, args.test_num, args.batch_size, stop_fn=stop_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -105,7 +103,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/examples/bipedal_hardcore_sac.py b/examples/bipedal_hardcore_sac.py index 3d9c435f7..7eb906e28 100644 --- a/examples/bipedal_hardcore_sac.py +++ b/examples/bipedal_hardcore_sac.py @@ -136,7 +136,6 @@ def save_fn(policy): args.step_per_epoch, args.collect_per_step, args.test_num, args.batch_size, stop_fn=IsStop, save_fn=save_fn, writer=writer) - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -144,7 +143,6 @@ def save_fn(policy): collector = Collector(policy, env) result = collector.collect(n_episode=16, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/examples/halfcheetahBullet_v0_sac.py b/examples/halfcheetahBullet_v0_sac.py index 3da77ccaa..0c947170a 100644 --- a/examples/halfcheetahBullet_v0_sac.py +++ b/examples/halfcheetahBullet_v0_sac.py @@ -102,8 +102,6 @@ def stop_fn(x): args.batch_size, stop_fn=stop_fn, writer=writer, log_interval=args.log_interval) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -111,7 +109,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/examples/point_maze_td3.py b/examples/point_maze_td3.py index bad71daac..6478c31a9 100644 --- a/examples/point_maze_td3.py +++ b/examples/point_maze_td3.py @@ -103,8 +103,6 @@ def stop_fn(x): args.step_per_epoch, args.collect_per_step, args.test_num, args.batch_size, stop_fn=stop_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -112,7 +110,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_step=1000, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/examples/pong_a2c.py b/examples/pong_a2c.py index 31e439dac..bd749fcea 100644 --- a/examples/pong_a2c.py +++ b/examples/pong_a2c.py @@ -90,8 +90,6 @@ def stop_fn(x): policy, train_collector, test_collector, args.epoch, args.step_per_epoch, args.collect_per_step, args.repeat_per_collect, args.test_num, args.batch_size, stop_fn=stop_fn, writer=writer) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -99,7 +97,6 @@ def stop_fn(x): collector = Collector(policy, env, preprocess_fn=preprocess_fn) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/examples/pong_dqn.py b/examples/pong_dqn.py index b404c9879..da31ecdb7 100644 --- a/examples/pong_dqn.py +++ b/examples/pong_dqn.py @@ -96,8 +96,6 @@ def test_fn(x): args.batch_size, train_fn=train_fn, test_fn=test_fn, stop_fn=stop_fn, writer=writer) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -105,7 +103,6 @@ def test_fn(x): collector = Collector(policy, env, preprocess_fn=preprocess_fn) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/examples/pong_ppo.py b/examples/pong_ppo.py index 5202a3815..9d6ede8ad 100644 --- a/examples/pong_ppo.py +++ b/examples/pong_ppo.py @@ -94,8 +94,6 @@ def stop_fn(x): policy, train_collector, test_collector, args.epoch, args.step_per_epoch, args.collect_per_step, args.repeat_per_collect, args.test_num, args.batch_size, stop_fn=stop_fn, writer=writer) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -103,7 +101,6 @@ def stop_fn(x): collector = Collector(policy, env, preprocess_fn=preprocess_fn) result = collector.collect(n_step=2000, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/examples/sac_mcc.py b/examples/sac_mcc.py index 8412f58fd..527800543 100644 --- a/examples/sac_mcc.py +++ b/examples/sac_mcc.py @@ -110,8 +110,6 @@ def stop_fn(x): args.step_per_epoch, args.collect_per_step, args.test_num, args.batch_size, stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -119,7 +117,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/test/continuous/test_ddpg.py b/test/continuous/test_ddpg.py index 0bbc697bf..457fcd592 100644 --- a/test/continuous/test_ddpg.py +++ b/test/continuous/test_ddpg.py @@ -104,8 +104,6 @@ def stop_fn(x): args.step_per_epoch, args.collect_per_step, args.test_num, args.batch_size, stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -113,7 +111,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/test/continuous/test_ppo.py b/test/continuous/test_ppo.py index 67c80c3c9..ed42e7901 100644 --- a/test/continuous/test_ppo.py +++ b/test/continuous/test_ppo.py @@ -119,8 +119,6 @@ def stop_fn(x): args.test_num, args.batch_size, stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -128,7 +126,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/test/continuous/test_sac_with_il.py b/test/continuous/test_sac_with_il.py index 9bc8ac543..dffebc70e 100644 --- a/test/continuous/test_sac_with_il.py +++ b/test/continuous/test_sac_with_il.py @@ -105,7 +105,6 @@ def stop_fn(x): args.step_per_epoch, args.collect_per_step, args.test_num, args.batch_size, stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -113,7 +112,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() # here we define an imitation collector with a trivial policy if args.task == 'Pendulum-v0': @@ -134,8 +132,6 @@ def stop_fn(x): args.step_per_epoch // 5, args.collect_per_step, args.test_num, args.batch_size, stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - il_test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -143,7 +139,6 @@ def stop_fn(x): collector = Collector(il_policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/test/continuous/test_td3.py b/test/continuous/test_td3.py index 2d036e04e..d2b95421e 100644 --- a/test/continuous/test_td3.py +++ b/test/continuous/test_td3.py @@ -109,8 +109,6 @@ def stop_fn(x): args.step_per_epoch, args.collect_per_step, args.test_num, args.batch_size, stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -118,7 +116,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/test/discrete/test_a2c_with_il.py b/test/discrete/test_a2c_with_il.py index 1ea935680..d99bc1448 100644 --- a/test/discrete/test_a2c_with_il.py +++ b/test/discrete/test_a2c_with_il.py @@ -94,7 +94,6 @@ def stop_fn(x): args.test_num, args.batch_size, stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -102,7 +101,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() # here we define an imitation collector with a trivial policy if args.task == 'CartPole-v0': @@ -122,8 +120,6 @@ def stop_fn(x): args.step_per_epoch, args.collect_per_step, args.test_num, args.batch_size, stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - il_test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -131,7 +127,6 @@ def stop_fn(x): collector = Collector(il_policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/test/discrete/test_dqn.py b/test/discrete/test_dqn.py index 642d535c2..53fbd4a22 100644 --- a/test/discrete/test_dqn.py +++ b/test/discrete/test_dqn.py @@ -102,8 +102,6 @@ def test_fn(x): stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -111,7 +109,6 @@ def test_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/test/discrete/test_drqn.py b/test/discrete/test_drqn.py index f4eb8326e..cb1858e3f 100644 --- a/test/discrete/test_drqn.py +++ b/test/discrete/test_drqn.py @@ -96,8 +96,6 @@ def test_fn(x): stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -105,7 +103,6 @@ def test_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/test/discrete/test_pdqn.py b/test/discrete/test_pdqn.py index 16c75719a..e6010766d 100644 --- a/test/discrete/test_pdqn.py +++ b/test/discrete/test_pdqn.py @@ -102,8 +102,6 @@ def test_fn(x): stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -111,7 +109,6 @@ def test_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/test/discrete/test_pg.py b/test/discrete/test_pg.py index 0bf4a8347..ee934a340 100644 --- a/test/discrete/test_pg.py +++ b/test/discrete/test_pg.py @@ -151,8 +151,6 @@ def stop_fn(x): args.test_num, args.batch_size, stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -160,7 +158,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/test/discrete/test_ppo.py b/test/discrete/test_ppo.py index b7b308c8d..515e2f225 100644 --- a/test/discrete/test_ppo.py +++ b/test/discrete/test_ppo.py @@ -108,8 +108,6 @@ def stop_fn(x): args.test_num, args.batch_size, stop_fn=stop_fn, save_fn=save_fn, writer=writer) assert stop_fn(result['best_reward']) - train_collector.close() - test_collector.close() if __name__ == '__main__': pprint.pprint(result) # Let's watch its performance! @@ -117,7 +115,6 @@ def stop_fn(x): collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() if __name__ == '__main__': diff --git a/test/multiagent/tic_tac_toe.py b/test/multiagent/tic_tac_toe.py index 3922d3ebf..5422c6e3b 100644 --- a/test/multiagent/tic_tac_toe.py +++ b/test/multiagent/tic_tac_toe.py @@ -159,9 +159,6 @@ def test_fn(x): stop_fn=stop_fn, save_fn=save_fn, writer=writer, test_in_train=False) - train_collector.close() - test_collector.close() - return result, policy.policies[args.agent_id - 1] @@ -175,4 +172,3 @@ def watch(args: argparse.Namespace = get_args(), collector = Collector(policy, env) result = collector.collect(n_episode=1, render=args.render) print(f'Final reward: {result["rew"]}, length: {result["len"]}') - collector.close() diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index fbd3d6719..fe8b16f10 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -167,10 +167,6 @@ def render(self, **kwargs) -> None: """Render all the environment(s).""" return self.env.render(**kwargs) - def close(self) -> None: - """Close the environment(s).""" - self.env.close() - def _reset_state(self, id: Union[int, List[int]]) -> None: """Reset the hidden state: self.data.state[id].""" state = self.data.state # it is a reference From 01f1a9620d2b49eb656031f7b21c6e111e969efe Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 5 Aug 2020 21:20:38 +0800 Subject: [PATCH 20/74] fix close --- test/throughput/test_batch_profile.py | 27 ++++++++++++----------- test/throughput/test_buffer_profile.py | 6 ++--- test/throughput/test_collector_profile.py | 17 ++++++-------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/test/throughput/test_batch_profile.py b/test/throughput/test_batch_profile.py index c36dd5f6f..9654f5838 100644 --- a/test/throughput/test_batch_profile.py +++ b/test/throughput/test_batch_profile.py @@ -29,10 +29,10 @@ def data(): batch3 = Batch(obs=[np.arange(20) for _ in np.arange(batch_len)], reward=np.arange(batch_len)) indexs = np.random.choice(batch_len, - size=batch_len//10, replace=False) + size=batch_len // 10, replace=False) slice_dict = {'obs': [np.arange(20) - for _ in np.arange(batch_len//10)], - 'reward': np.arange(batch_len//10)} + for _ in np.arange(batch_len // 10)], + 'reward': np.arange(batch_len // 10)} dict_set = [{'obs': np.arange(20), 'info': "this is info", 'reward': 0} for _ in np.arange(1e2)] batch4 = Batch( @@ -45,16 +45,17 @@ def data(): ) print("Initialised") - return {'batch_set': batch_set, - 'batch0': batch0, - 'batchs1': batchs1, - 'batchs2': batchs2, - 'batch3': batch3, - 'indexs': indexs, - 'dict_set': dict_set, - 'slice_dict': slice_dict, - 'batch4': batch4 - } + return { + 'batch_set': batch_set, + 'batch0': batch0, + 'batchs1': batchs1, + 'batchs2': batchs2, + 'batch3': batch3, + 'indexs': indexs, + 'dict_set': dict_set, + 'slice_dict': slice_dict, + 'batch4': batch4 + } def test_init(data): diff --git a/test/throughput/test_buffer_profile.py b/test/throughput/test_buffer_profile.py index aec32682a..939c24fdf 100644 --- a/test/throughput/test_buffer_profile.py +++ b/test/throughput/test_buffer_profile.py @@ -8,15 +8,15 @@ @pytest.fixture(scope="module") def data(): np.random.seed(0) - obs = {'observable': np.random.rand( - 100, 100), 'hidden': np.random.randint(1000, size=200)} + obs = {'observable': np.random.rand(100, 100), + 'hidden': np.random.randint(1000, size=200)} info = {'policy': "dqn", 'base': np.arange(10)} add_data = {'obs': obs, 'rew': 1., 'act': np.random.rand(30), 'done': False, 'obs_next': obs, 'info': info} buffer = ReplayBuffer(int(1e3), stack_num=100) buffer2 = ReplayBuffer(int(1e4), stack_num=100) indexes = np.random.choice(int(1e3), size=3, replace=False) - return{ + return { 'add_data': add_data, 'buffer': buffer, 'buffer2': buffer2, diff --git a/test/throughput/test_collector_profile.py b/test/throughput/test_collector_profile.py index 49ada853e..f9d8a3e4e 100644 --- a/test/throughput/test_collector_profile.py +++ b/test/throughput/test_collector_profile.py @@ -48,7 +48,7 @@ def learn(self, batch, **kwargs): return super().learn(batch, **kwargs) def forward(self, batch, state=None, **kwargs): - return Batch(act=np.array([30]*len(batch)), state=None, logits=None) + return Batch(act=np.array([30] * len(batch)), state=None, logits=None) @pytest.fixture(scope="module") @@ -70,7 +70,7 @@ def data(): collector = Collector(policy, env, ReplayBuffer(50000)) collector_vec = Collector(policy, env_vec, ReplayBuffer(50000)) collector_subproc = Collector(policy, env_subproc, ReplayBuffer(50000)) - return{ + return { "env": env, "env_vec": env_vec, "env_subproc": env_subproc, @@ -79,14 +79,13 @@ def data(): "buffer": buffer, "collector": collector, "collector_vec": collector_vec, - "collector_subproc": collector_subproc - } + "collector_subproc": collector_subproc, + } def test_init(data): for _ in range(5000): - c = Collector(data["policy"], data["env"], data["buffer"]) - c.close() + Collector(data["policy"], data["env"], data["buffer"]) def test_reset(data): @@ -111,8 +110,7 @@ def test_sample(data): def test_init_vec_env(data): for _ in range(5000): - c = Collector(data["policy"], data["env_vec"], data["buffer"]) - c.close() + Collector(data["policy"], data["env_vec"], data["buffer"]) def test_reset_vec_env(data): @@ -137,8 +135,7 @@ def test_sample_vec_env(data): def test_init_subproc_env(data): for _ in range(5000): - c = Collector(data["policy"], data["env_subproc_init"], data["buffer"]) - c.close() + Collector(data["policy"], data["env_subproc_init"], data["buffer"]) def test_reset_subproc_env(data): From 51f0cb408265cedabbf7f32ccd92eeac049549c3 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 5 Aug 2020 22:16:32 +0800 Subject: [PATCH 21/74] try import --- docs/api/tianshou.env.rst | 5 +++ tianshou/env/__init__.py | 9 +++--- tianshou/env/venvs.py | 31 +++++++++---------- tianshou/env/worker/__init__.py | 11 +++++++ tianshou/env/worker/base.py | 10 +++--- tianshou/env/worker/dummy.py | 23 ++++++-------- tianshou/env/worker/ray.py | 27 +++++++++------- tianshou/env/worker/subproc.py | 55 +++++++++++++++++---------------- tianshou/exploration/random.py | 2 +- 9 files changed, 95 insertions(+), 78 deletions(-) diff --git a/docs/api/tianshou.env.rst b/docs/api/tianshou.env.rst index fa3343472..7201bae46 100644 --- a/docs/api/tianshou.env.rst +++ b/docs/api/tianshou.env.rst @@ -5,3 +5,8 @@ tianshou.env :members: :undoc-members: :show-inheritance: + +.. automodule:: tianshou.env.worker + :members: + :undoc-members: + :show-inheritance: diff --git a/tianshou/env/__init__.py b/tianshou/env/__init__.py index 4fefdf08d..0fa4d15b5 100644 --- a/tianshou/env/__init__.py +++ b/tianshou/env/__init__.py @@ -1,14 +1,13 @@ -from tianshou.env.venvs import \ - (BaseVectorEnv, DummyVectorEnv, VectorEnv, SubprocVectorEnv, - ShmemVectorEnv, RayVectorEnv) +from tianshou.env.venvs import BaseVectorEnv, DummyVectorEnv, VectorEnv, \ + SubprocVectorEnv, ShmemVectorEnv, RayVectorEnv from tianshou.env.maenv import MultiAgentEnv __all__ = [ 'BaseVectorEnv', 'DummyVectorEnv', - 'VectorEnv', + 'VectorEnv', # TODO: remove in later version 'SubprocVectorEnv', - 'RayVectorEnv', 'ShmemVectorEnv', + 'RayVectorEnv', 'MultiAgentEnv', ] diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 4135ebc47..226c37381 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -1,10 +1,10 @@ import gym -import numpy as np import warnings +import numpy as np from typing import List, Tuple, Union, Optional, Callable, Any -from tianshou.env.worker.base import EnvWorker -from tianshou.env.worker.subproc import SubProcEnvWorker -from tianshou.env.worker.dummy import DummyEnvWorker + +from tianshou.env.worker import EnvWorker, DummyEnvWorker, SubprocEnvWorker, \ + RayEnvWorker def run_once(f): @@ -66,7 +66,7 @@ def seed(self, seed): Otherwise, the outputs of these envs may be the same with each other. - :param wait_num: used in asynchronous simulation if the time cost of + :param int wait_num: used in asynchronous simulation if the time cost of ``env.step`` varies with time and synchronously waiting for all environments to finish a step is time-wasting. In that case, we can return when ``wait_num`` environments finish a step and keep on @@ -240,9 +240,9 @@ def render(self, **kwargs) -> List[Any]: @run_once def close(self) -> List[Any]: - """Close all of the environments. This function will be called - only once (if not, it will be called during garbage collected). - This way, ``close`` of all workers can be assured. + """Close all of the environments. This function will be called only + once (if not, it will be called during garbage collected). This way, + ``close`` of all workers can be assured. """ if self.is_async: # finish remaining steps, and close @@ -264,7 +264,7 @@ def __init__(self, env_fns: List[Callable[[], gym.Env]], wait_num: Optional[int] = None, ) -> None: - super(DummyVectorEnv, self).__init__( + super().__init__( env_fns, lambda fn: DummyEnvWorker(fn), wait_num=wait_num, @@ -279,7 +279,7 @@ def __init__(self, warnings.warn( 'VectorEnv is renamed to DummyVectorEnv, and will be removed' ' in 0.3. Use DummyVectorEnv instead!', DeprecationWarning) - super(VectorEnv, self).__init__( + super().__init__( env_fns, lambda fn: DummyEnvWorker(fn), wait_num=wait_num, @@ -299,16 +299,16 @@ def __init__(self, env_fns: List[Callable[[], gym.Env]], wait_num: Optional[int] = None, ) -> None: - super(SubprocVectorEnv, self).__init__( + super().__init__( env_fns, - lambda fn: SubProcEnvWorker(fn), + lambda fn: SubprocEnvWorker(fn), wait_num=wait_num, ) class ShmemVectorEnv(BaseVectorEnv): """Optimized version of SubprocVectorEnv that uses shared variables to - communicate observations. SubprocVectorEnv has exactly the same API as + communicate observations. ShmemVectorEnv has exactly the same API as SubprocVectorEnv. .. seealso:: @@ -321,9 +321,9 @@ def __init__(self, env_fns: List[Callable[[], gym.Env]], wait_num: Optional[int] = None, ) -> None: - super(ShmemVectorEnv, self).__init__( + super().__init__( env_fns, - lambda fn: SubProcEnvWorker(fn, share_memory=True), + lambda fn: SubprocEnvWorker(fn, share_memory=True), wait_num=wait_num, ) @@ -352,7 +352,6 @@ def __init__(self, if not ray.is_initialized(): ray.init() - from tianshou.env.worker.ray import RayEnvWorker super().__init__( env_fns, lambda fn: RayEnvWorker(fn), diff --git a/tianshou/env/worker/__init__.py b/tianshou/env/worker/__init__.py index e69de29bb..a3d2ea5f3 100644 --- a/tianshou/env/worker/__init__.py +++ b/tianshou/env/worker/__init__.py @@ -0,0 +1,11 @@ +from tianshou.env.worker.base import EnvWorker +from tianshou.env.worker.dummy import DummyEnvWorker +from tianshou.env.worker.subproc import SubprocEnvWorker +from tianshou.env.worker.ray import RayEnvWorker + +__all__ = [ + 'EnvWorker', + 'DummyEnvWorker', + 'SubprocEnvWorker', + 'RayEnvWorker', +] diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index ddb32f0a2..9e5599b32 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -5,8 +5,7 @@ class EnvWorker(ABC, gym.Env): - """An abstract worker for an environment. - """ + """An abstract worker for an environment.""" def __init__(self, env_fn: Callable[[], gym.Env]) -> None: self._env_fn = env_fn @@ -35,8 +34,7 @@ def get_result(self def step(self, action: np.ndarray ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - """ - ``send_action`` and ``get_result`` are coupled in sync simulation, + """``send_action`` and ``get_result`` are coupled in sync simulation, so typically users only call ``step`` function. But they can be called separately in async simulation, i.e. someone calls ``send_action`` first, and calls ``get_result`` later. @@ -50,6 +48,7 @@ def seed(self, seed: Optional[int] = None): @abstractmethod def render(self, **kwargs) -> None: + """Renders the environment.""" pass @abstractmethod @@ -58,7 +57,6 @@ def close(self) -> Any: @staticmethod def wait(workers: List['EnvWorker']) -> List['EnvWorker']: - """ - Given a list of workers, return those ready ones. + """Given a list of workers, return those ready ones. """ raise NotImplementedError diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index d5ad91c71..6b6a5f521 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -1,24 +1,15 @@ -from typing import List, Callable, Optional, Any - import gym import numpy as np +from typing import List, Callable, Optional, Any -from tianshou.env.worker.base import EnvWorker +from tianshou.env.worker import EnvWorker class DummyEnvWorker(EnvWorker): - """ - Dummy worker used in sequential vector environments - """ - - @staticmethod - def wait(workers: List['DummyEnvWorker'] - ) -> List['DummyEnvWorker']: - # SequentialEnvWorker objects are always ready - return workers + """Dummy worker used in sequential vector environments.""" def __init__(self, env_fn: Callable[[], gym.Env]) -> None: - super(DummyEnvWorker, self).__init__(env_fn) + super().__init__(env_fn) self.env = env_fn() def __getattr__(self, key: str): @@ -26,6 +17,12 @@ def __getattr__(self, key: str): return getattr(self.env, key) return None + @staticmethod + def wait(workers: List['DummyEnvWorker'] + ) -> List['DummyEnvWorker']: + # SequentialEnvWorker objects are always ready + return workers + def reset(self): return self.env.reset() diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index 5a9fad37c..490137f12 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -1,13 +1,25 @@ -from typing import List, Callable, Tuple, Optional, Any - import gym import numpy as np -import ray +from typing import List, Callable, Tuple, Optional, Any -from tianshou.env.worker.base import EnvWorker +from tianshou.env.worker import EnvWorker + +try: + import ray +except ImportError: + pass class RayEnvWorker(EnvWorker): + """Ray worker used in RayVectorEnv.""" + + def __init__(self, env_fn: Callable[[], gym.Env]) -> None: + super().__init__(env_fn) + self.env = ray.remote(gym.Wrapper).options(num_cpus=0).remote(env_fn()) + + def __getattr__(self, key: str): + return ray.get(self.env.__getattr__.remote(key)) + @staticmethod def wait(workers: List['RayEnvWorker']) -> List['RayEnvWorker']: ready_envs, _ = ray.wait( @@ -16,13 +28,6 @@ def wait(workers: List['RayEnvWorker']) -> List['RayEnvWorker']: timeout=0) return [workers[ready_envs.index(env)] for env in ready_envs] - def __init__(self, env_fn: Callable[[], gym.Env]) -> None: - super(RayEnvWorker, self).__init__(env_fn) - self.env = ray.remote(gym.Wrapper).options(num_cpus=0).remote(env_fn()) - - def __getattr__(self, key: str): - return ray.get(self.env.__getattr__.remote(key)) - def reset(self): return ray.get(self.env.reset.remote()) diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index c57cc7532..a92d0dd8d 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -1,14 +1,14 @@ +import gym import ctypes +import numpy as np from collections import OrderedDict -from multiprocessing import Array, Pipe, connection from multiprocessing.context import Process +from multiprocessing import Array, Pipe, connection from typing import Callable, Any, List, Tuple, Optional -import gym -import numpy as np +from tianshou.env.worker import EnvWorker from tianshou.env.utils import CloudpickleWrapper -from tianshou.env.worker.base import EnvWorker def _worker(parent, p, env_fn_wrapper, obs_bufs=None): @@ -59,18 +59,20 @@ def _encode_obs(obs, buffer): p.close() -_NP_TO_CT = {np.bool: ctypes.c_bool, - np.bool_: ctypes.c_bool, - np.uint8: ctypes.c_uint8, - np.uint16: ctypes.c_uint16, - np.uint32: ctypes.c_uint32, - np.uint64: ctypes.c_uint64, - np.int8: ctypes.c_int8, - np.int16: ctypes.c_int16, - np.int32: ctypes.c_int32, - np.int64: ctypes.c_int64, - np.float32: ctypes.c_float, - np.float64: ctypes.c_double} +_NP_TO_CT = { + np.bool: ctypes.c_bool, + np.bool_: ctypes.c_bool, + np.uint8: ctypes.c_uint8, + np.uint16: ctypes.c_uint16, + np.uint32: ctypes.c_uint32, + np.uint64: ctypes.c_uint64, + np.int8: ctypes.c_int8, + np.int16: ctypes.c_int16, + np.int32: ctypes.c_int32, + np.int64: ctypes.c_int64, + np.float32: ctypes.c_float, + np.float64: ctypes.c_double, +} class ShArray: @@ -92,11 +94,12 @@ def get(self): dtype=self.dtype).reshape(self.shape) -class SubProcEnvWorker(EnvWorker): +class SubprocEnvWorker(EnvWorker): + """Subprocess worker used in SubprocVectorEnv and ShmemVectorEnv.""" def __init__(self, env_fn: Callable[[], gym.Env], share_memory=False ) -> None: - super(SubProcEnvWorker, self).__init__(env_fn) + super().__init__(env_fn) self.parent_remote, self.child_remote = Pipe() self.share_memory = share_memory self.buffer = None @@ -105,22 +108,26 @@ def __init__(self, env_fn: Callable[[], gym.Env], share_memory=False obs_space = dummy.observation_space dummy.close() del dummy - self.buffer = SubProcEnvWorker._setup_buf(obs_space) + self.buffer = SubprocEnvWorker._setup_buf(obs_space) args = (self.parent_remote, self.child_remote, CloudpickleWrapper(env_fn), self.buffer) self.process = Process(target=_worker, args=args, daemon=True) self.process.start() self.child_remote.close() + def __getattr__(self, key: str): + self.parent_remote.send(['getattr', key]) + return self.parent_remote.recv() + @staticmethod def _setup_buf(space): if isinstance(space, gym.spaces.Dict): assert isinstance(space.spaces, OrderedDict) - buffer = {k: SubProcEnvWorker._setup_buf(v) + buffer = {k: SubprocEnvWorker._setup_buf(v) for k, v in space.spaces.items()} elif isinstance(space, gym.spaces.Tuple): assert isinstance(space.spaces, tuple) - buffer = tuple([SubProcEnvWorker._setup_buf(t) + buffer = tuple([SubprocEnvWorker._setup_buf(t) for t in space.spaces]) else: buffer = ShArray(space.dtype, space.shape) @@ -152,15 +159,11 @@ def close(self) -> Any: return result @staticmethod - def wait(workers: List['SubProcEnvWorker']) -> List['SubProcEnvWorker']: + def wait(workers: List['SubprocEnvWorker']) -> List['SubprocEnvWorker']: conns = [x.parent_remote for x in workers] ready_conns = connection.wait(conns) return [workers[conns.index(con)] for con in ready_conns] - def __getattr__(self, key: str): - self.parent_remote.send(['getattr', key]) - return self.parent_remote.recv() - def get_result(self ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: obs, rew, done, info = self.parent_remote.recv() diff --git a/tianshou/exploration/random.py b/tianshou/exploration/random.py index 2df5489f9..34fd50399 100644 --- a/tianshou/exploration/random.py +++ b/tianshou/exploration/random.py @@ -7,7 +7,7 @@ class BaseNoise(ABC, object): """The action noise base class.""" def __init__(self, **kwargs) -> None: - super(BaseNoise, self).__init__() + super().__init__() @abstractmethod def __call__(self, **kwargs) -> np.ndarray: From d328b2ef57fd5803f2eb28067a8d0ac539a59fba Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 5 Aug 2020 22:28:25 +0800 Subject: [PATCH 22/74] polish venvs --- tianshou/env/venvs.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 226c37381..89fd06c86 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -150,10 +150,10 @@ def step(self, id: Optional[Union[int, List[int]]] = None ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Run one timestep of all the environments’ dynamics if id is - ``None``, otherwise run one timestep for some environments - with given id, either an int or a list. When the end of - episode is reached, you are responsible for calling reset(id) - to reset this environment’s state. + ``None``, otherwise run one timestep for some environments with given + id, either an int or a list. When the end of episode is reached, you + are responsible for calling reset(id) to reset this environment’s + state. Accept a batch of action and return a tuple (obs, rew, done, info). @@ -264,11 +264,7 @@ def __init__(self, env_fns: List[Callable[[], gym.Env]], wait_num: Optional[int] = None, ) -> None: - super().__init__( - env_fns, - lambda fn: DummyEnvWorker(fn), - wait_num=wait_num, - ) + super().__init__(env_fns, DummyEnvWorker, wait_num=wait_num) class VectorEnv(BaseVectorEnv): @@ -279,11 +275,7 @@ def __init__(self, warnings.warn( 'VectorEnv is renamed to DummyVectorEnv, and will be removed' ' in 0.3. Use DummyVectorEnv instead!', DeprecationWarning) - super().__init__( - env_fns, - lambda fn: DummyEnvWorker(fn), - wait_num=wait_num, - ) + super().__init__(env_fns, DummyEnvWorker, wait_num=wait_num) class SubprocVectorEnv(BaseVectorEnv): @@ -301,7 +293,7 @@ def __init__(self, ) -> None: super().__init__( env_fns, - lambda fn: SubprocEnvWorker(fn), + lambda fn: SubprocEnvWorker(fn, share_memory=False), wait_num=wait_num, ) @@ -352,8 +344,4 @@ def __init__(self, if not ray.is_initialized(): ray.init() - super().__init__( - env_fns, - lambda fn: RayEnvWorker(fn), - wait_num=wait_num, - ) + super().__init__(env_fns, RayEnvWorker, wait_num=wait_num) From a30ae27aaa71501bf9bea553201bff52f4545b17 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 5 Aug 2020 22:46:09 +0800 Subject: [PATCH 23/74] fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0403599fa..732336f3b 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ Within this API, we can interact with different policies conveniently. ### Elegant and Flexible -Currently, the overall code of Tianshou platform is less than 1500 lines without environment wrappers for Atari and Mujoco. Most of the implemented algorithms are less than 100 lines of python code. It is quite easy to go through the framework and understand how it works. We provide many flexible API as you wish, for instance, if you want to use your policy to interact with the environment with (at least) `n` steps: +Currently, the overall code of Tianshou platform is less than 2500 lines. Most of the implemented algorithms are less than 100 lines of python code. It is quite easy to go through the framework and understand how it works. We provide many flexible API as you wish, for instance, if you want to use your policy to interact with the environment with (at least) `n` steps: ```python result = collector.collect(n_step=n) From be93183f903a5267789f53e24013e8c124396a52 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Thu, 6 Aug 2020 19:27:40 +0800 Subject: [PATCH 24/74] _batch_set_item bugfix --- tianshou/data/collector.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index fe8b16f10..ac42a5d93 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -375,18 +375,22 @@ def _batch_set_item(source: Batch, indices: np.ndarray, # 4. both source[k] and target[k] do not exist or are reserved, do nothing. # A special case in case 4, if target[k] is reserved but source[k] does # not exist, make source[k] reserved, too. - for k, v in target.items(): - if not isinstance(v, Batch) or not v.is_empty(): + for k, vt in target.items(): + if not isinstance(vt, Batch) or not vt.is_empty(): # target[k] is non-reserved vs = source.get(k, Batch()) - if isinstance(vs, Batch) and vs.is_empty(): - # case 2 - # use __dict__ to avoid many type checks - source.__dict__[k] = _create_value(v[0], size) + if isinstance(vs, Batch): + if vs.is_empty(): + # case 2 + # use __dict__ to avoid many type checks + source.__dict__[k] = _create_value(vt[0], size) + else: + assert isinstance(vt, Batch) + _batch_set_item(source.__dict__[k], indices, vt, size) else: # target[k] is reserved # case 1 or special case of case 4 if k not in source.__dict__: source.__dict__[k] = Batch() continue - source.__dict__[k][indices] = v + source.__dict__[k][indices] = vt From 1efc253f04099f727ebcfd2b7d0c08b0af4b1178 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Thu, 6 Aug 2020 19:48:15 +0800 Subject: [PATCH 25/74] rm pdqn --- test/discrete/test_pdqn.py | 115 ------------------------------------- 1 file changed, 115 deletions(-) delete mode 100644 test/discrete/test_pdqn.py diff --git a/test/discrete/test_pdqn.py b/test/discrete/test_pdqn.py deleted file mode 100644 index e6010766d..000000000 --- a/test/discrete/test_pdqn.py +++ /dev/null @@ -1,115 +0,0 @@ -import os -import gym -import torch -import pprint -import argparse -import numpy as np -from torch.utils.tensorboard import SummaryWriter - -from tianshou.utils.net.common import Net -from tianshou.env import DummyVectorEnv -from tianshou.policy import DQNPolicy -from tianshou.trainer import offpolicy_trainer -from tianshou.data import Collector, ReplayBuffer, PrioritizedReplayBuffer - - -def get_args(): - parser = argparse.ArgumentParser() - parser.add_argument('--task', type=str, default='CartPole-v0') - parser.add_argument('--seed', type=int, default=1626) - parser.add_argument('--eps-test', type=float, default=0.05) - parser.add_argument('--eps-train', type=float, default=0.1) - parser.add_argument('--buffer-size', type=int, default=20000) - parser.add_argument('--lr', type=float, default=1e-3) - parser.add_argument('--gamma', type=float, default=0.9) - parser.add_argument('--n-step', type=int, default=3) - parser.add_argument('--target-update-freq', type=int, default=320) - parser.add_argument('--epoch', type=int, default=10) - parser.add_argument('--step-per-epoch', type=int, default=1000) - parser.add_argument('--collect-per-step', type=int, default=10) - parser.add_argument('--batch-size', type=int, default=64) - parser.add_argument('--layer-num', type=int, default=3) - parser.add_argument('--training-num', type=int, default=8) - parser.add_argument('--test-num', type=int, default=100) - parser.add_argument('--logdir', type=str, default='log') - parser.add_argument('--render', type=float, default=0.) - parser.add_argument('--prioritized-replay', type=int, default=1) - parser.add_argument('--alpha', type=float, default=0.5) - parser.add_argument('--beta', type=float, default=0.5) - parser.add_argument( - '--device', type=str, - default='cuda' if torch.cuda.is_available() else 'cpu') - args = parser.parse_known_args()[0] - return args - - -def test_pdqn(args=get_args()): - env = gym.make(args.task) - args.state_shape = env.observation_space.shape or env.observation_space.n - args.action_shape = env.action_space.shape or env.action_space.n - # train_envs = gym.make(args.task) - # you can also use tianshou.env.SubprocVectorEnv - train_envs = DummyVectorEnv( - [lambda: gym.make(args.task) for _ in range(args.training_num)]) - # test_envs = gym.make(args.task) - test_envs = DummyVectorEnv( - [lambda: gym.make(args.task) for _ in range(args.test_num)]) - # seed - np.random.seed(args.seed) - torch.manual_seed(args.seed) - train_envs.seed(args.seed) - test_envs.seed(args.seed) - # model - net = Net(args.layer_num, args.state_shape, - args.action_shape, args.device).to(args.device) - optim = torch.optim.Adam(net.parameters(), lr=args.lr) - policy = DQNPolicy( - net, optim, args.gamma, args.n_step, - target_update_freq=args.target_update_freq) - # collector - if args.prioritized_replay > 0: - buf = PrioritizedReplayBuffer( - args.buffer_size, alpha=args.alpha, - beta=args.alpha, repeat_sample=True) - else: - buf = ReplayBuffer(args.buffer_size) - train_collector = Collector( - policy, train_envs, buf) - test_collector = Collector(policy, test_envs) - # policy.set_eps(1) - train_collector.collect(n_step=args.batch_size) - # log - log_path = os.path.join(args.logdir, args.task, 'dqn') - writer = SummaryWriter(log_path) - - def save_fn(policy): - torch.save(policy.state_dict(), os.path.join(log_path, 'policy.pth')) - - def stop_fn(x): - return x >= env.spec.reward_threshold - - def train_fn(x): - policy.set_eps(args.eps_train) - - def test_fn(x): - policy.set_eps(args.eps_test) - - # trainer - result = offpolicy_trainer( - policy, train_collector, test_collector, args.epoch, - args.step_per_epoch, args.collect_per_step, args.test_num, - args.batch_size, train_fn=train_fn, test_fn=test_fn, - stop_fn=stop_fn, save_fn=save_fn, writer=writer) - - assert stop_fn(result['best_reward']) - if __name__ == '__main__': - pprint.pprint(result) - # Let's watch its performance! - env = gym.make(args.task) - collector = Collector(policy, env) - result = collector.collect(n_episode=1, render=args.render) - print(f'Final reward: {result["rew"]}, length: {result["len"]}') - - -if __name__ == '__main__': - test_pdqn(get_args()) From 80b6f41f51570aeb04b2e6e8d9c685429014af33 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Thu, 6 Aug 2020 20:58:56 +0800 Subject: [PATCH 26/74] ensure termination of subproc --- tianshou/env/venvs.py | 8 -------- tianshou/env/worker/subproc.py | 4 ++++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 89fd06c86..ca3d1c65f 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -250,14 +250,6 @@ def close(self) -> List[Any]: self.step(None) return [w.close() for w in self.workers] - def __del__(self): - """Close the environment before garbage collected""" - try: - self.close() - except RuntimeError: - # it has already been closed - pass - class DummyVectorEnv(BaseVectorEnv): def __init__(self, diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index a92d0dd8d..e3f3fe09f 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -184,3 +184,7 @@ def reset(self): def seed(self, seed: Optional[int] = None): self.parent_remote.send(['seed', seed]) return self.parent_remote.recv() + + def __del__(self): + # ensure the subproc is terminated + self.process.terminate() From 0dddbee88865ed6d50585790712bdc2c195ec3e6 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sun, 9 Aug 2020 09:06:25 +0800 Subject: [PATCH 27/74] find a bug in collector --- test/base/test_collector.py | 32 +++++++++++++++---------------- tianshou/env/venvs.py | 38 ++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/test/base/test_collector.py b/test/base/test_collector.py index 3e4d4004a..91461b91e 100644 --- a/test/base/test_collector.py +++ b/test/base/test_collector.py @@ -19,7 +19,7 @@ def __init__(self, dict_state=False): def forward(self, batch, state=None): if self.dict_state: return Batch(act=np.ones(len(batch.obs['index']))) - return Batch(act=np.ones(len(batch.obs))) + return Batch(act=np.ones(len(batch.obs)), state=np.array([1, 2])) def learn(self): pass @@ -72,26 +72,26 @@ def test_collector(): c0 = Collector(policy, env, ReplayBuffer(size=100, ignore_obs_next=False), logger.preprocess_fn) c0.collect(n_step=3) - assert np.allclose(c0.buffer.obs[:4], np.expand_dims( - [0, 1, 0, 1], axis=-1)) - assert np.allclose(c0.buffer[:4].obs_next, np.expand_dims( - [1, 2, 1, 2], axis=-1)) + assert np.allclose(c0.buffer.obs[:4], + np.expand_dims([0, 1, 0, 1], axis=-1)) + assert np.allclose(c0.buffer[:4].obs_next, + np.expand_dims([1, 2, 1, 2], axis=-1)) c0.collect(n_episode=3) - assert np.allclose(c0.buffer.obs[:10], np.expand_dims( - [0, 1, 0, 1, 0, 1, 0, 1, 0, 1], axis=-1)) - assert np.allclose(c0.buffer[:10].obs_next, np.expand_dims( - [1, 2, 1, 2, 1, 2, 1, 2, 1, 2], axis=-1)) + assert np.allclose(c0.buffer.obs[:10], + np.expand_dims([0, 1, 0, 1, 0, 1, 0, 1, 0, 1], axis=-1)) + assert np.allclose(c0.buffer[:10].obs_next, + np.expand_dims([1, 2, 1, 2, 1, 2, 1, 2, 1, 2], axis=-1)) c0.collect(n_step=3, random=True) c1 = Collector(policy, venv, ReplayBuffer(size=100, ignore_obs_next=False), logger.preprocess_fn) c1.collect(n_step=6) assert np.allclose(c1.buffer.obs[:11], np.expand_dims( - [0, 1, 0, 1, 2, 0, 1, 0, 1, 2, 3], axis=-1)) - assert np.allclose(c1.buffer[:11].obs_next, np.expand_dims([ - 1, 2, 1, 2, 3, 1, 2, 1, 2, 3, 4], axis=-1)) + [0, 1, 0, 1, 2, 0, 1, 0, 1, 2, 3], axis=-1)) + assert np.allclose(c1.buffer[:11].obs_next, np.expand_dims( + [1, 2, 1, 2, 3, 1, 2, 1, 2, 3, 4], axis=-1)) c1.collect(n_episode=2) - assert np.allclose(c1.buffer.obs[11:21], np.expand_dims( - [0, 1, 2, 3, 4, 0, 1, 0, 1, 2], axis=-1)) + assert np.allclose(c1.buffer.obs[11:21], + np.expand_dims([0, 1, 2, 3, 4, 0, 1, 0, 1, 2], axis=-1)) assert np.allclose(c1.buffer[11:21].obs_next, np.expand_dims([1, 2, 3, 4, 5, 1, 2, 1, 2, 3], axis=-1)) c1.collect(n_episode=3, random=True) @@ -116,7 +116,7 @@ def test_collector_with_async(): env_fns = [lambda x=i: MyTestEnv(size=x, sleep=0.1, random_sleep=True) for i in env_lens] - venv = SubprocVectorEnv(env_fns, wait_num=len(env_fns)) + venv = SubprocVectorEnv(env_fns, wait_num=len(env_fns) - 1) policy = MyPolicy() c1 = Collector(policy, venv, ReplayBuffer(size=1000, ignore_obs_next=False), @@ -129,8 +129,6 @@ def test_collector_with_async(): size = len(c1.buffer) obs = c1.buffer.obs[:size] done = c1.buffer.done[:size] - print(env_id[:size]) - print(obs) obs_ground_truth = [] i = 0 while i < size: diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index ca3d1c65f..9c4eb000a 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -65,14 +65,16 @@ def seed(self, seed): Otherwise, the outputs of these envs may be the same with each other. - + :param env_fns: a list of callable envs, ``env_fns[i]()`` will generate + the ith env. + :param worker_fn: a callable worker, ``worker_fn(env_fns[i])`` will + generate a worker which contains this env. :param int wait_num: used in asynchronous simulation if the time cost of ``env.step`` varies with time and synchronously waiting for all environments to finish a step is time-wasting. In that case, we can return when ``wait_num`` environments finish a step and keep on simulation in these environments. If ``None``, asynchronous simulation is disabled; else, ``1 <= wait_num <= env_num``. - """ def __init__(self, @@ -88,7 +90,7 @@ def __init__(self, self.wait_num = wait_num or len(env_fns) assert 1 <= self.wait_num <= len(env_fns), \ f'wait_num should be in [1, {len(env_fns)}], but got {wait_num}' - self.is_async = wait_num is not None + self.is_async = self.wait_num != len(env_fns) self.waiting_conn = [] # environments in self.ready_id is actually ready # but environments in self.waiting_id are just waiting when checked, @@ -117,31 +119,32 @@ def __getattr__(self, key: str): environment class.""" return [getattr(worker, key) for worker in self.workers] - def _assert_and_transform_id(self, - id: Optional[Union[int, List[int]]] = None - ) -> List[int]: + def _wrap_id(self, + id: Optional[Union[int, List[int]]] = None + ) -> List[int]: if id is None: id = list(range(self.env_num)) elif np.isscalar(id): id = [id] + return id + + def _assert_id(self, + id: Optional[Union[int, List[int]]] = None + ) -> List[int]: for i in id: assert i not in self.waiting_id, \ f'Cannot manipulate environment {i} which is stepping now!' assert i in self.ready_id, \ f'Can only manipulate ready environments {self.ready_id}.' - return id def reset(self, id: Optional[Union[int, List[int]]] = None): """Reset the state of all the environments and return initial observations if id is ``None``, otherwise reset the specific environments with given id, either an int or a list. """ - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] + id = self._wrap_id(id) if self.is_async: - id = self._assert_and_transform_id(id) + self._assert_id(id) obs = np.stack([self.workers[i].reset() for i in id]) return obs @@ -178,11 +181,8 @@ def step(self, (initially they are env_ids of all the environments). If action is ``None``, fetch unfinished step() calls instead. """ + id = self._wrap_id(id) if not self.is_async: - if id is None: - id = range(self.env_num) - elif np.isscalar(id): - id = [id] assert len(action) == len(id) for i, j in enumerate(id): self.workers[j].send_action(action[i]) @@ -191,7 +191,7 @@ def step(self, return obs, rew, done, info else: if action is not None: - id = self._assert_and_transform_id(id) + self._assert_id(id) assert len(action) == len(id) for i, (act, env_id) in enumerate(zip(action, id)): self.workers[env_id].send_action(act) @@ -265,8 +265,8 @@ def __init__(self, wait_num: Optional[int] = None, ) -> None: warnings.warn( - 'VectorEnv is renamed to DummyVectorEnv, and will be removed' - ' in 0.3. Use DummyVectorEnv instead!', DeprecationWarning) + 'VectorEnv is renamed to DummyVectorEnv, and will be removed in ' + '0.3. Use DummyVectorEnv instead!', DeprecationWarning) super().__init__(env_fns, DummyEnvWorker, wait_num=wait_num) From 09b487a5e21b4219269b0a9d6e4417ce852b0f93 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Sun, 9 Aug 2020 10:23:30 +0800 Subject: [PATCH 28/74] bugfix for MyPolicy and collector --- test/base/test_collector.py | 14 +++++++++++--- tianshou/data/collector.py | 4 +++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/test/base/test_collector.py b/test/base/test_collector.py index 91461b91e..5c76896b3 100644 --- a/test/base/test_collector.py +++ b/test/base/test_collector.py @@ -12,14 +12,22 @@ class MyPolicy(BasePolicy): - def __init__(self, dict_state=False): + def __init__(self, dict_state=False, need_state: bool = True): + """ + ``dict_state`` means the observation of the environment is a dict + ``need_state`` means the state of the policy (typically in rnn) + """ super().__init__() self.dict_state = dict_state def forward(self, batch, state=None): + if state is None: + state = np.zeros((len(batch.obs), 2)) + else: + state += 1 if self.dict_state: - return Batch(act=np.ones(len(batch.obs['index']))) - return Batch(act=np.ones(len(batch.obs)), state=np.array([1, 2])) + return Batch(act=np.ones(len(batch.obs['index'])), state=state) + return Batch(act=np.ones(len(batch.obs)), state=state) def learn(self): pass diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index ac42a5d93..54416ae81 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -130,6 +130,8 @@ def _default_rew_metric(x): def reset(self) -> None: """Reset all related variables in the collector.""" + # use empty Batch for ``state`` so that ``self.data`` supports slicing + # convert empty Batch to None when passing data to policy self.data = Batch(state={}, obs={}, act={}, rew={}, done={}, info={}, obs_next={}, policy={}) self.reset_env() @@ -235,7 +237,7 @@ def collect(self, # restore the state and the input data last_state = self.data.state - if last_state.is_empty(): + if isinstance(last_state, Batch) and last_state.is_empty(): last_state = None self.data.update(state=Batch(), obs_next=Batch(), policy=Batch()) From ecf7a76c6a38171a87a2089fe30bc4a7b2a11171 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sun, 9 Aug 2020 11:28:21 +0800 Subject: [PATCH 29/74] polish venvs --- tianshou/env/venvs.py | 95 +++++++++++++++---------------------- tianshou/env/worker/base.py | 4 +- 2 files changed, 39 insertions(+), 60 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 9c4eb000a..628dc0cf9 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -15,8 +15,7 @@ def run_once(f): def wrapper(self, *args, **kwargs): if self.unique_id in f.has_run_objects: - raise RuntimeError( - f'{f} can be called only once for object {self}') + raise RuntimeError(f'{f} can be called only once for {self}.') f.has_run_objects.add(self.unique_id) return f(self, *args, **kwargs) return wrapper @@ -105,42 +104,42 @@ def __len__(self) -> int: """Return len(self), which is the number of environments.""" return self.env_num - def __getattribute__(self, key: str): - """Switch between the default attribute getter or one - looking at wrapped environment level depending on the key.""" + def __getattribute__(self, key: str) -> Any: + """Switch between the default attribute getter or one looking at + wrapped environment level depending on the key. + """ if key not in ('observation_space', 'action_space'): return super().__getattribute__(key) else: return self.__getattr__(key) - def __getattr__(self, key: str): + def __getattr__(self, key: str) -> Any: """Try to retrieve an attribute from each individual wrapped - environment, if it does not belong to the wrapping vector - environment class.""" + environment, if it does not belong to the wrapping vector environment + class. + """ return [getattr(worker, key) for worker in self.workers] - def _wrap_id(self, - id: Optional[Union[int, List[int]]] = None - ) -> List[int]: + def _wrap_id( + self, id: Optional[Union[int, List[int]]] = None) -> List[int]: if id is None: id = list(range(self.env_num)) elif np.isscalar(id): id = [id] return id - def _assert_id(self, - id: Optional[Union[int, List[int]]] = None - ) -> List[int]: + def _assert_id( + self, id: Optional[Union[int, List[int]]] = None) -> List[int]: for i in id: assert i not in self.waiting_id, \ f'Cannot manipulate environment {i} which is stepping now!' assert i in self.ready_id, \ f'Can only manipulate ready environments {self.ready_id}.' - def reset(self, id: Optional[Union[int, List[int]]] = None): + def reset(self, id: Optional[Union[int, List[int]]] = None) -> np.ndarray: """Reset the state of all the environments and return initial observations if id is ``None``, otherwise reset the specific - environments with given id, either an int or a list. + environments with the given id, either an int or a list. """ id = self._wrap_id(id) if self.is_async: @@ -152,11 +151,10 @@ def step(self, action: Optional[np.ndarray], id: Optional[Union[int, List[int]]] = None ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - """Run one timestep of all the environments’ dynamics if id is - ``None``, otherwise run one timestep for some environments with given - id, either an int or a list. When the end of episode is reached, you - are responsible for calling reset(id) to reset this environment’s - state. + """Run one timestep of all the environments’ dynamics if id is "None", + otherwise run one timestep for some environments with given id, either + an int or a list. When the end of episode is reached, you are + responsible for calling reset(id) to reset this environment’s state. Accept a batch of action and return a tuple (obs, rew, done, info). @@ -187,8 +185,6 @@ def step(self, for i, j in enumerate(id): self.workers[j].send_action(action[i]) result = [self.workers[j].get_result() for j in id] - obs, rew, done, info = map(np.stack, zip(*result)) - return obs, rew, done, info else: if action is not None: self._assert_id(id) @@ -205,13 +201,11 @@ def step(self, waiting_index = self.waiting_conn.index(conn) self.waiting_conn.pop(waiting_index) env_id = self.waiting_id.pop(waiting_index) - ans = conn.get_result() - obs, rew, done, info = ans + obs, rew, done, info = conn.get_result() info["env_id"] = env_id result.append((obs, rew, done, info)) self.ready_id.append(env_id) - obs, rew, done, info = map(np.stack, zip(*result)) - return obs, rew, done, info + return map(np.stack, zip(*result)) def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: """Set the seed for all environments. @@ -252,18 +246,14 @@ def close(self) -> List[Any]: class DummyVectorEnv(BaseVectorEnv): - def __init__(self, - env_fns: List[Callable[[], gym.Env]], - wait_num: Optional[int] = None, - ) -> None: + def __init__(self, env_fns: List[Callable[[], gym.Env]], + wait_num: Optional[int] = None) -> None: super().__init__(env_fns, DummyEnvWorker, wait_num=wait_num) class VectorEnv(BaseVectorEnv): - def __init__(self, - env_fns: List[Callable[[], gym.Env]], - wait_num: Optional[int] = None, - ) -> None: + def __init__(self, env_fns: List[Callable[[], gym.Env]], + wait_num: Optional[int] = None) -> None: warnings.warn( 'VectorEnv is renamed to DummyVectorEnv, and will be removed in ' '0.3. Use DummyVectorEnv instead!', DeprecationWarning) @@ -279,15 +269,11 @@ class SubprocVectorEnv(BaseVectorEnv): explanation. """ - def __init__(self, - env_fns: List[Callable[[], gym.Env]], - wait_num: Optional[int] = None, - ) -> None: - super().__init__( - env_fns, - lambda fn: SubprocEnvWorker(fn, share_memory=False), - wait_num=wait_num, - ) + def __init__(self, env_fns: List[Callable[[], gym.Env]], + wait_num: Optional[int] = None) -> None: + def worker_fn(fn): + return SubprocEnvWorker(fn, share_memory=False) + super().__init__(env_fns, worker_fn, wait_num=wait_num) class ShmemVectorEnv(BaseVectorEnv): @@ -301,15 +287,11 @@ class ShmemVectorEnv(BaseVectorEnv): detailed explanation. """ - def __init__(self, - env_fns: List[Callable[[], gym.Env]], - wait_num: Optional[int] = None, - ) -> None: - super().__init__( - env_fns, - lambda fn: SubprocEnvWorker(fn, share_memory=True), - wait_num=wait_num, - ) + def __init__(self, env_fns: List[Callable[[], gym.Env]], + wait_num: Optional[int] = None) -> None: + def worker_fn(fn): + return SubprocEnvWorker(fn, share_memory=True) + super().__init__(env_fns, worker_fn, wait_num=wait_num) class RayVectorEnv(BaseVectorEnv): @@ -323,17 +305,14 @@ class RayVectorEnv(BaseVectorEnv): explanation. """ - def __init__(self, - env_fns: List[Callable[[], gym.Env]], - wait_num: Optional[int] = None, - ) -> None: + def __init__(self, env_fns: List[Callable[[], gym.Env]], + wait_num: Optional[int] = None) -> None: try: import ray except ImportError as e: raise ImportError( 'Please install ray to support RayVectorEnv: pip install ray' ) from e - if not ray.is_initialized(): ray.init() super().__init__(env_fns, RayEnvWorker, wait_num=wait_num) diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 9e5599b32..0708b183d 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -17,11 +17,11 @@ def __getattribute__(self, key: str): return self.__getattr__(key) @abstractmethod - def __getattr__(self, key: str): + def __getattr__(self, key: str) -> Any: pass @abstractmethod - def reset(self): + def reset(self) -> Any: pass @abstractmethod From 0cb303d848513b04eba2c8f45047b7c76bdf7fb2 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sun, 9 Aug 2020 15:06:50 +0800 Subject: [PATCH 30/74] fix map --- tianshou/env/venvs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 628dc0cf9..e53df6311 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -205,7 +205,7 @@ def step(self, info["env_id"] = env_id result.append((obs, rew, done, info)) self.ready_id.append(env_id) - return map(np.stack, zip(*result)) + return list(map(np.stack, zip(*result))) def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: """Set the seed for all environments. From 302199e7b07727297b08c3768002507db5d4ac0f Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sun, 9 Aug 2020 16:13:25 +0800 Subject: [PATCH 31/74] need_state, venv.close(), collect random with async --- test/base/test_collector.py | 16 +++++++++------- test/base/test_env.py | 1 + test/discrete/test_drqn.py | 2 +- tianshou/data/collector.py | 10 ++-------- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/test/base/test_collector.py b/test/base/test_collector.py index 5c76896b3..604f449fa 100644 --- a/test/base/test_collector.py +++ b/test/base/test_collector.py @@ -12,19 +12,21 @@ class MyPolicy(BasePolicy): - def __init__(self, dict_state=False, need_state: bool = True): + def __init__(self, dict_state: bool = False, need_state: bool = True): """ - ``dict_state`` means the observation of the environment is a dict - ``need_state`` means the state of the policy (typically in rnn) + :param bool dict_state: if the observation of the environment is a dict + :param bool need_state: if the policy needs the hidden state (for RNN) """ super().__init__() self.dict_state = dict_state + self.need_state = need_state def forward(self, batch, state=None): - if state is None: - state = np.zeros((len(batch.obs), 2)) - else: - state += 1 + if self.need_state: + if state is None: + state = np.zeros((len(batch.obs), 2)) + else: + state += 1 if self.dict_state: return Batch(act=np.ones(len(batch.obs['index'])), state=state) return Batch(act=np.ones(len(batch.obs)), state=state) diff --git a/test/base/test_env.py b/test/base/test_env.py index d74c82f02..06296df58 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -66,6 +66,7 @@ def test_async_env(num=8, sleep=0.1): env_ids = env_ids[: len(action)] spent_time = time.time() - spent_time data = Batch.cat(o) + v.close() # assure 1/7 improvement assert spent_time < 6.0 * sleep * num / (num + 1) return spent_time, data diff --git a/test/discrete/test_drqn.py b/test/discrete/test_drqn.py index cb1858e3f..c4d976715 100644 --- a/test/discrete/test_drqn.py +++ b/test/discrete/test_drqn.py @@ -48,7 +48,7 @@ def test_drqn(args=get_args()): # train_envs = gym.make(args.task) # you can also use tianshou.env.SubprocVectorEnv train_envs = DummyVectorEnv( - [lambda: gym.make(args.task)for _ in range(args.training_num)]) + [lambda: gym.make(args.task) for _ in range(args.training_num)]) # test_envs = gym.make(args.task) test_envs = DummyVectorEnv( [lambda: gym.make(args.task) for _ in range(args.test_num)]) diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index 54416ae81..4c41e7b00 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -112,6 +112,7 @@ def __init__(self, self.policy = policy self.preprocess_fn = preprocess_fn self.process_fn = policy.process_fn + self._action_space = env.action_space self._action_noise = action_noise self._rew_metric = reward_metric or Collector._default_rew_metric # avoid creating attribute outside __init__ @@ -243,14 +244,7 @@ def collect(self, # calculate the next action if random: - if self.is_async: - # TODO self.env.action_space will invoke remote call for - # all environments, which may hang in async simulation. - # This can be avoided by using a random policy, but not - # in the collector level. Leave it as a future work. - raise RuntimeError("cannot use random " - "sampling in async simulation!") - spaces = self.env.action_space + spaces = self._action_space result = Batch( act=[spaces[i].sample() for i in self._ready_env_ids]) else: From 5129b0ad3a975a4230553d50d097c21903233d68 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sun, 9 Aug 2020 16:38:27 +0800 Subject: [PATCH 32/74] fix state inconsistency in collector --- test/base/test_collector.py | 4 ++-- tianshou/data/collector.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/base/test_collector.py b/test/base/test_collector.py index 604f449fa..272471752 100644 --- a/test/base/test_collector.py +++ b/test/base/test_collector.py @@ -193,7 +193,7 @@ def test_collector_with_dict_state(): Logger.single_preprocess_fn) c2.collect(n_episode=[0, 0, 0, 10]) batch = c2.sample(10) - print(batch['obs_next']['index']) + batch['obs_next']['index'] def test_collector_with_ma(): @@ -235,7 +235,7 @@ def reward_metric(x): r = c2.collect(n_episode=[0, 0, 0, 10])['rew'] assert np.asanyarray(r).size == 1 and r == 4. batch = c2.sample(10) - print(batch['obs_next']) + batch['obs_next'] if __name__ == '__main__': diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index 4c41e7b00..4a2a69f29 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -257,7 +257,9 @@ def collect(self, state = Batch() self.data.update(state=state, policy=result.get('policy', Batch())) # save hidden state to policy._state, in order to save into buffer - self.data.policy._state = self.data.state + if not (isinstance(self.data.state, Batch) + and self.data.state.is_empty()): + self.data.policy._state = self.data.state self.data.act = to_numpy(result.act) if self._action_noise is not None: From 2f911013b9fafd39d18307e450c42632a3975ae8 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sun, 9 Aug 2020 17:40:20 +0800 Subject: [PATCH 33/74] adjust position and complete annotation --- tianshou/env/venvs.py | 19 ++++++++----- tianshou/env/worker/base.py | 17 ++++++------ tianshou/env/worker/dummy.py | 12 ++++----- tianshou/env/worker/ray.py | 16 +++++------ tianshou/env/worker/subproc.py | 49 +++++++++++++++++----------------- 5 files changed, 59 insertions(+), 54 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index e53df6311..49ba87a48 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -246,18 +246,25 @@ def close(self) -> List[Any]: class DummyVectorEnv(BaseVectorEnv): + """Dummy vectorized environment wrapper, implemented in for-loop. + + .. seealso:: + + Please refer to :class:`~tianshou.env.BaseVectorEnv` for more detailed + explanation. + """ + def __init__(self, env_fns: List[Callable[[], gym.Env]], wait_num: Optional[int] = None) -> None: super().__init__(env_fns, DummyEnvWorker, wait_num=wait_num) -class VectorEnv(BaseVectorEnv): - def __init__(self, env_fns: List[Callable[[], gym.Env]], - wait_num: Optional[int] = None) -> None: +class VectorEnv(DummyVectorEnv): + def __init__(self, *args, **kwargs) -> None: warnings.warn( 'VectorEnv is renamed to DummyVectorEnv, and will be removed in ' - '0.3. Use DummyVectorEnv instead!', DeprecationWarning) - super().__init__(env_fns, DummyEnvWorker, wait_num=wait_num) + '0.3. Use DummyVectorEnv instead!', Warning) + super().__init__(*args, **kwargs) class SubprocVectorEnv(BaseVectorEnv): @@ -277,7 +284,7 @@ def worker_fn(fn): class ShmemVectorEnv(BaseVectorEnv): - """Optimized version of SubprocVectorEnv that uses shared variables to + """Optimized version of SubprocVectorEnv which uses shared variables to communicate observations. ShmemVectorEnv has exactly the same API as SubprocVectorEnv. diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 0708b183d..7d66faf11 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -25,7 +25,7 @@ def reset(self) -> Any: pass @abstractmethod - def send_action(self, action: np.ndarray): + def send_action(self, action: np.ndarray) -> None: pass def get_result(self @@ -42,21 +42,20 @@ def step(self, action: np.ndarray self.send_action(action) return self.get_result() + @staticmethod + def wait(workers: List['EnvWorker']) -> List['EnvWorker']: + """Given a list of workers, return those ready ones.""" + raise NotImplementedError + @abstractmethod - def seed(self, seed: Optional[int] = None): + def seed(self, seed: Optional[int] = None) -> int: pass @abstractmethod - def render(self, **kwargs) -> None: + def render(self, **kwargs) -> Any: """Renders the environment.""" pass @abstractmethod def close(self) -> Any: pass - - @staticmethod - def wait(workers: List['EnvWorker']) -> List['EnvWorker']: - """Given a list of workers, return those ready ones. - """ - raise NotImplementedError diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index 6b6a5f521..2038f2057 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -17,22 +17,22 @@ def __getattr__(self, key: str): return getattr(self.env, key) return None + def reset(self) -> Any: + return self.env.reset() + @staticmethod def wait(workers: List['DummyEnvWorker'] ) -> List['DummyEnvWorker']: # SequentialEnvWorker objects are always ready return workers - def reset(self): - return self.env.reset() - - def send_action(self, action: np.ndarray): + def send_action(self, action: np.ndarray) -> None: self.result = self.env.step(action) - def seed(self, seed: Optional[int] = None): + def seed(self, seed: Optional[int] = None) -> List[int]: return self.env.seed(seed) if hasattr(self.env, 'seed') else None - def render(self, **kwargs) -> None: + def render(self, **kwargs) -> Any: return self.env.render(**kwargs) if \ hasattr(self.env, 'render') else None diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index 490137f12..51b3c4d6f 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -20,6 +20,9 @@ def __init__(self, env_fn: Callable[[], gym.Env]) -> None: def __getattr__(self, key: str): return ray.get(self.env.__getattr__.remote(key)) + def reset(self) -> Any: + return ray.get(self.env.reset.remote()) + @staticmethod def wait(workers: List['RayEnvWorker']) -> List['RayEnvWorker']: ready_envs, _ = ray.wait( @@ -28,23 +31,20 @@ def wait(workers: List['RayEnvWorker']) -> List['RayEnvWorker']: timeout=0) return [workers[ready_envs.index(env)] for env in ready_envs] - def reset(self): - return ray.get(self.env.reset.remote()) + def send_action(self, action: np.ndarray) -> None: + # self.action is actually a handle + self.result = self.env.step.remote(action) def get_result(self ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: return ray.get(self.result) - def send_action(self, action: np.ndarray): - # self.action is actually a handle - self.result = self.env.step.remote(action) - - def seed(self, seed: Optional[int] = None): + def seed(self, seed: Optional[int] = None) -> List[int]: if hasattr(self.env, 'seed'): return ray.get(self.env.seed.remote(seed)) return None - def render(self, **kwargs) -> None: + def render(self, **kwargs) -> Any: if hasattr(self.env, 'render'): return ray.get(self.env.render.remote(**kwargs)) return None diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index e3f3fe09f..d4e84c9d9 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -28,8 +28,7 @@ def _encode_obs(obs, buffer): while True: try: cmd, data = p.recv() - except EOFError: - # the pipe has been closed + except EOFError: # the pipe has been closed p.close() break if cmd == 'step': @@ -145,18 +144,12 @@ def decode_obs(buffer): raise NotImplementedError return decode_obs(self.buffer) - def render(self, **kwargs) -> None: - self.parent_remote.send(['render', kwargs]) - return self.parent_remote.recv() - - def close(self) -> Any: - try: - self.parent_remote.send(['close', None]) - result = self.parent_remote.recv() - except (BrokenPipeError, EOFError): - result = None - self.process.join() - return result + def reset(self) -> Any: + self.parent_remote.send(['reset', None]) + obs = self.parent_remote.recv() + if self.share_memory: + obs = self._decode_obs(obs) + return obs @staticmethod def wait(workers: List['SubprocEnvWorker']) -> List['SubprocEnvWorker']: @@ -164,6 +157,9 @@ def wait(workers: List['SubprocEnvWorker']) -> List['SubprocEnvWorker']: ready_conns = connection.wait(conns) return [workers[conns.index(con)] for con in ready_conns] + def send_action(self, action: np.ndarray) -> None: + self.parent_remote.send(['step', action]) + def get_result(self ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: obs, rew, done, info = self.parent_remote.recv() @@ -171,20 +167,23 @@ def get_result(self obs = self._decode_obs(obs) return obs, rew, done, info - def send_action(self, action: np.ndarray): - self.parent_remote.send(['step', action]) - - def reset(self): - self.parent_remote.send(['reset', None]) - obs = self.parent_remote.recv() - if self.share_memory: - obs = self._decode_obs(obs) - return obs - - def seed(self, seed: Optional[int] = None): + def seed(self, seed: Optional[int] = None) -> List[int]: self.parent_remote.send(['seed', seed]) return self.parent_remote.recv() + def render(self, **kwargs) -> Any: + self.parent_remote.send(['render', kwargs]) + return self.parent_remote.recv() + + def close(self) -> Any: + try: + self.parent_remote.send(['close', None]) + result = self.parent_remote.recv() + except (BrokenPipeError, EOFError): + result = None + self.process.join() + return result + def __del__(self): # ensure the subproc is terminated self.process.terminate() From 731b26024a8c5a0b97d2da8893dfd7c29c78a34c Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sun, 9 Aug 2020 17:46:37 +0800 Subject: [PATCH 34/74] update docs --- docs/tutorials/cheatsheet.rst | 2 ++ tianshou/env/worker/dummy.py | 2 +- tianshou/env/worker/ray.py | 2 +- tianshou/env/worker/subproc.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/cheatsheet.rst b/docs/tutorials/cheatsheet.rst index 91e4ddd13..dd074a1f3 100644 --- a/docs/tutorials/cheatsheet.rst +++ b/docs/tutorials/cheatsheet.rst @@ -48,6 +48,8 @@ where ``env_fns`` is a list of callable env hooker. The above code can be writte env_fns = [lambda x=i: MyTestEnv(size=x) for i in [2, 3, 4, 5]] venv = SubprocVectorEnv(env_fns) +All of these VectorEnv class have async mode (related to issue #103), where we can give it an extra parameter ``wait_num``. If we have 4 envs and set ``wait_num = 3``, in each of the step in VectorEnv it only returns 3 results of these 4 envs. This mode eases the case where each step cost vary different time, e.g. 90% step cost 1s, but 10% cost 10s. + .. warning:: If you use your own environment, please make sure the ``seed`` method is set up properly, e.g., diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index 2038f2057..48b3bbffa 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -29,7 +29,7 @@ def wait(workers: List['DummyEnvWorker'] def send_action(self, action: np.ndarray) -> None: self.result = self.env.step(action) - def seed(self, seed: Optional[int] = None) -> List[int]: + def seed(self, seed: Optional[int] = None) -> int: return self.env.seed(seed) if hasattr(self.env, 'seed') else None def render(self, **kwargs) -> Any: diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index 51b3c4d6f..83f274a31 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -39,7 +39,7 @@ def get_result(self ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: return ray.get(self.result) - def seed(self, seed: Optional[int] = None) -> List[int]: + def seed(self, seed: Optional[int] = None) -> int: if hasattr(self.env, 'seed'): return ray.get(self.env.seed.remote(seed)) return None diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index d4e84c9d9..1784abd36 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -167,7 +167,7 @@ def get_result(self obs = self._decode_obs(obs) return obs, rew, done, info - def seed(self, seed: Optional[int] = None) -> List[int]: + def seed(self, seed: Optional[int] = None) -> int: self.parent_remote.send(['seed', seed]) return self.parent_remote.recv() From d15579b1213aff419ca472470460aec16a0fc499 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sun, 9 Aug 2020 20:17:04 +0800 Subject: [PATCH 35/74] venv close --- tianshou/env/venvs.py | 48 +++++++++++++++------------------- tianshou/env/worker/base.py | 12 ++++++++- tianshou/env/worker/dummy.py | 2 +- tianshou/env/worker/ray.py | 2 +- tianshou/env/worker/subproc.py | 11 ++++---- 5 files changed, 39 insertions(+), 36 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 49ba87a48..b3291feb9 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -7,28 +7,6 @@ RayEnvWorker -def run_once(f): - """Run once decorator for a method in a class. Each instance can run the - method at most once. - """ - f.has_run_objects = set() - - def wrapper(self, *args, **kwargs): - if self.unique_id in f.has_run_objects: - raise RuntimeError(f'{f} can be called only once for {self}.') - f.has_run_objects.add(self.unique_id) - return f(self, *args, **kwargs) - return wrapper - - -def generate_id(): - generate_id.i += 1 - return generate_id.i - - -generate_id.i = 0 - - class BaseVectorEnv(gym.Env): """Base class for vectorized environments wrapper. Usage: :: @@ -98,7 +76,11 @@ def __init__(self, self.waiting_id = [] # all environments are ready in the beginning self.ready_id = list(range(self.env_num)) - self.unique_id = generate_id() + self.is_closed = False + + def _assert_is_closed(self): + assert not self.is_closed, f"Methods of {self.__class__.__name__} "\ + "should not be called after close." def __len__(self) -> int: """Return len(self), which is the number of environments.""" @@ -141,6 +123,7 @@ def reset(self, id: Optional[Union[int, List[int]]] = None) -> np.ndarray: observations if id is ``None``, otherwise reset the specific environments with the given id, either an int or a list. """ + self._assert_is_closed() id = self._wrap_id(id) if self.is_async: self._assert_id(id) @@ -179,6 +162,7 @@ def step(self, (initially they are env_ids of all the environments). If action is ``None``, fetch unfinished step() calls instead. """ + self._assert_is_closed() id = self._wrap_id(id) if not self.is_async: assert len(action) == len(id) @@ -217,6 +201,7 @@ def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: generators. The first value in the list should be the "main" seed, or \ the value which a reproducer pass to "seed". """ + self._assert_is_closed() if np.isscalar(seed): seed = [seed + _ for _ in range(self.env_num)] elif seed is None: @@ -226,24 +211,33 @@ def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: def render(self, **kwargs) -> List[Any]: """Render all of the environments.""" + self._assert_is_closed(self.render) if self.is_async and len(self.waiting_id) > 0: raise RuntimeError( f"Environments {self.waiting_id} are still " f"stepping, cannot render them now.") return [w.render(**kwargs) for w in self.workers] - @run_once def close(self) -> List[Any]: """Close all of the environments. This function will be called only once (if not, it will be called during garbage collected). This way, ``close`` of all workers can be assured. """ + self._assert_is_closed() if self.is_async: - # finish remaining steps, and close - if len(self.waiting_conn) > 0: - self.step(None) + try: + # finish remaining steps, and close + if len(self.waiting_conn) > 0: + self.step(None) + except TypeError: # self.step -> self.worker.wait doesn't exist + pass + self.is_closed = True return [w.close() for w in self.workers] + def __del__(self) -> List[Any]: + if not self.is_closed: + self.close() + class DummyVectorEnv(BaseVectorEnv): """Dummy vectorized environment wrapper, implemented in for-loop. diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 7d66faf11..a73de8769 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -9,6 +9,7 @@ class EnvWorker(ABC, gym.Env): def __init__(self, env_fn: Callable[[], gym.Env]) -> None: self._env_fn = env_fn + self.is_closed = False def __getattribute__(self, key: str): if key not in ('observation_space', 'action_space'): @@ -57,5 +58,14 @@ def render(self, **kwargs) -> Any: pass @abstractmethod - def close(self) -> Any: + def close_env(self) -> Any: pass + + def close(self) -> Any: + if self.is_closed: + return None + self.is_closed = True + return self.close_env() + + def __del__(self) -> Any: + return self.close() diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index 48b3bbffa..9f5770fc9 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -36,5 +36,5 @@ def render(self, **kwargs) -> Any: return self.env.render(**kwargs) if \ hasattr(self.env, 'render') else None - def close(self) -> Any: + def close_env(self) -> Any: return self.env.close() diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index 83f274a31..6e35adcbd 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -49,5 +49,5 @@ def render(self, **kwargs) -> Any: return ray.get(self.env.render.remote(**kwargs)) return None - def close(self) -> Any: + def close_env(self) -> Any: return ray.get(self.env.close.remote()) diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index 1784abd36..7e1634ade 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -175,15 +175,14 @@ def render(self, **kwargs) -> Any: self.parent_remote.send(['render', kwargs]) return self.parent_remote.recv() - def close(self) -> Any: + def close_env(self) -> Any: try: self.parent_remote.send(['close', None]) + # mp may be deleted so it may raise AttributeError result = self.parent_remote.recv() - except (BrokenPipeError, EOFError): + self.process.join() + except (BrokenPipeError, EOFError, AttributeError): result = None - self.process.join() - return result - - def __del__(self): # ensure the subproc is terminated self.process.terminate() + return result From 698ca97eaead294263db26d476a6b60d4e3250d5 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sun, 9 Aug 2020 20:24:21 +0800 Subject: [PATCH 36/74] minor fix --- test/base/test_collector.py | 2 -- tianshou/data/collector.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/test/base/test_collector.py b/test/base/test_collector.py index 272471752..c69925378 100644 --- a/test/base/test_collector.py +++ b/test/base/test_collector.py @@ -193,7 +193,6 @@ def test_collector_with_dict_state(): Logger.single_preprocess_fn) c2.collect(n_episode=[0, 0, 0, 10]) batch = c2.sample(10) - batch['obs_next']['index'] def test_collector_with_ma(): @@ -235,7 +234,6 @@ def reward_metric(x): r = c2.collect(n_episode=[0, 0, 0, 10])['rew'] assert np.asanyarray(r).size == 1 and r == 4. batch = c2.sample(10) - batch['obs_next'] if __name__ == '__main__': diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index 4a2a69f29..f00bc514d 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -379,8 +379,7 @@ def _batch_set_item(source: Batch, indices: np.ndarray, vs = source.get(k, Batch()) if isinstance(vs, Batch): if vs.is_empty(): - # case 2 - # use __dict__ to avoid many type checks + # case 2, use __dict__ to avoid many type checks source.__dict__[k] = _create_value(vt[0], size) else: assert isinstance(vt, Batch) From fdec4932cdfa906f91408043307a9333d6b82fff Mon Sep 17 00:00:00 2001 From: n+e <463003665@qq.com> Date: Sun, 9 Aug 2020 20:35:27 +0800 Subject: [PATCH 37/74] Update docs/tutorials/cheatsheet.rst --- docs/tutorials/cheatsheet.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/cheatsheet.rst b/docs/tutorials/cheatsheet.rst index dd074a1f3..c4d0ab9cd 100644 --- a/docs/tutorials/cheatsheet.rst +++ b/docs/tutorials/cheatsheet.rst @@ -48,7 +48,7 @@ where ``env_fns`` is a list of callable env hooker. The above code can be writte env_fns = [lambda x=i: MyTestEnv(size=x) for i in [2, 3, 4, 5]] venv = SubprocVectorEnv(env_fns) -All of these VectorEnv class have async mode (related to issue #103), where we can give it an extra parameter ``wait_num``. If we have 4 envs and set ``wait_num = 3``, in each of the step in VectorEnv it only returns 3 results of these 4 envs. This mode eases the case where each step cost vary different time, e.g. 90% step cost 1s, but 10% cost 10s. +All subclasses of ``VectorEnv`` have an async mode (related to issue #103), where we can give it an extra parameter ``wait_num``. If we have 4 envs and set ``wait_num = 3``, each of the step in VectorEnv only returns 3 results of these 4 envs. This mode eases the case where each step cost varies at different timescale, e.g. 90% step cost 1s, but 10% cost 10s. .. warning:: From 3c0dec7969bc855316b10756816d9a04f8f78751 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sun, 9 Aug 2020 20:38:56 +0800 Subject: [PATCH 38/74] _assert_is_NOT_closed --- tianshou/env/venvs.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index b3291feb9..d95e17479 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -78,7 +78,7 @@ def __init__(self, self.ready_id = list(range(self.env_num)) self.is_closed = False - def _assert_is_closed(self): + def _assert_is_not_closed(self): assert not self.is_closed, f"Methods of {self.__class__.__name__} "\ "should not be called after close." @@ -123,7 +123,7 @@ def reset(self, id: Optional[Union[int, List[int]]] = None) -> np.ndarray: observations if id is ``None``, otherwise reset the specific environments with the given id, either an int or a list. """ - self._assert_is_closed() + self._assert_is_not_closed() id = self._wrap_id(id) if self.is_async: self._assert_id(id) @@ -162,7 +162,7 @@ def step(self, (initially they are env_ids of all the environments). If action is ``None``, fetch unfinished step() calls instead. """ - self._assert_is_closed() + self._assert_is_not_closed() id = self._wrap_id(id) if not self.is_async: assert len(action) == len(id) @@ -201,7 +201,7 @@ def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: generators. The first value in the list should be the "main" seed, or \ the value which a reproducer pass to "seed". """ - self._assert_is_closed() + self._assert_is_not_closed() if np.isscalar(seed): seed = [seed + _ for _ in range(self.env_num)] elif seed is None: @@ -211,7 +211,7 @@ def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: def render(self, **kwargs) -> List[Any]: """Render all of the environments.""" - self._assert_is_closed(self.render) + self._assert_is_not_closed() if self.is_async and len(self.waiting_id) > 0: raise RuntimeError( f"Environments {self.waiting_id} are still " @@ -223,7 +223,7 @@ def close(self) -> List[Any]: once (if not, it will be called during garbage collected). This way, ``close`` of all workers can be assured. """ - self._assert_is_closed() + self._assert_is_not_closed() if self.is_async: try: # finish remaining steps, and close From c08d6ea8745082c84c41ed8de8d16ecbbb871f65 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Mon, 10 Aug 2020 08:26:44 +0800 Subject: [PATCH 39/74] small fix --- docs/tutorials/cheatsheet.rst | 6 +++--- test/base/env.py | 1 + tianshou/env/venvs.py | 23 ++++++++++++----------- tianshou/env/worker/base.py | 13 +++++-------- tianshou/env/worker/dummy.py | 7 +++---- tianshou/env/worker/ray.py | 8 ++++---- tianshou/env/worker/subproc.py | 16 +++++++++------- 7 files changed, 37 insertions(+), 37 deletions(-) diff --git a/docs/tutorials/cheatsheet.rst b/docs/tutorials/cheatsheet.rst index c4d0ab9cd..71aa68d3a 100644 --- a/docs/tutorials/cheatsheet.rst +++ b/docs/tutorials/cheatsheet.rst @@ -48,7 +48,7 @@ where ``env_fns`` is a list of callable env hooker. The above code can be writte env_fns = [lambda x=i: MyTestEnv(size=x) for i in [2, 3, 4, 5]] venv = SubprocVectorEnv(env_fns) -All subclasses of ``VectorEnv`` have an async mode (related to issue #103), where we can give it an extra parameter ``wait_num``. If we have 4 envs and set ``wait_num = 3``, each of the step in VectorEnv only returns 3 results of these 4 envs. This mode eases the case where each step cost varies at different timescale, e.g. 90% step cost 1s, but 10% cost 10s. +All subclasses of :class:`~tianshou.env.BaseVectorEnv` have an async mode (related to issue #103), where we can give it an extra parameter ``wait_num``. If we have 4 envs and set ``wait_num = 3``, each of the step in VectorEnv only returns 3 results of these 4 envs. This mode eases the case where each step cost varies at different timescale, e.g. 90% step cost 1s, but 10% cost 10s. .. warning:: @@ -141,9 +141,9 @@ First of all, your self-defined environment must follow the Gym's API, some of t - step(action) -> state, reward, done, info -- seed(s) -> None +- seed(s) -> List[int] -- render(mode) -> None +- render(mode) -> Any - close() -> None diff --git a/test/base/env.py b/test/base/env.py index b84282f05..cc0991072 100644 --- a/test/base/env.py +++ b/test/base/env.py @@ -46,6 +46,7 @@ def __init__(self, size, sleep=0, dict_state=False, recurse_state=False, def seed(self, seed=0): self.rng = np.random.RandomState(seed) + return [seed] def reset(self, state=0): self.done = False diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index d95e17479..b9b97b039 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -114,9 +114,9 @@ def _assert_id( self, id: Optional[Union[int, List[int]]] = None) -> List[int]: for i in id: assert i not in self.waiting_id, \ - f'Cannot manipulate environment {i} which is stepping now!' + f'Cannot interact with environment {i} which is stepping now.' assert i in self.ready_id, \ - f'Can only manipulate ready environments {self.ready_id}.' + f'Can only interact with ready environments {self.ready_id}.' def reset(self, id: Optional[Union[int, List[int]]] = None) -> np.ndarray: """Reset the state of all the environments and return initial @@ -191,23 +191,23 @@ def step(self, self.ready_id.append(env_id) return list(map(np.stack, zip(*result))) - def seed(self, seed: Optional[Union[int, List[int]]] = None) -> List[int]: + def seed(self, + seed: Optional[Union[int, List[int]]] = None) -> List[List[int]]: """Set the seed for all environments. Accept ``None``, an int (which will extend ``i`` to ``[i, i + 1, i + 2, ...]``) or a list. - :return: The list of seeds used in this env's random number \ - generators. The first value in the list should be the "main" seed, or \ - the value which a reproducer pass to "seed". + :return: The list of seeds used in this env's random number generators. + The first value in the list should be the "main" seed, or the value + which a reproducer pass to "seed". """ self._assert_is_not_closed() if np.isscalar(seed): seed = [seed + _ for _ in range(self.env_num)] elif seed is None: seed = [seed] * self.env_num - result = [w.seed(s) for w, s in zip(self.workers, seed)] - return result + return [w.seed(s) for w, s in zip(self.workers, seed)] def render(self, **kwargs) -> List[Any]: """Render all of the environments.""" @@ -218,7 +218,7 @@ def render(self, **kwargs) -> List[Any]: f"stepping, cannot render them now.") return [w.render(**kwargs) for w in self.workers] - def close(self) -> List[Any]: + def close(self) -> None: """Close all of the environments. This function will be called only once (if not, it will be called during garbage collected). This way, ``close`` of all workers can be assured. @@ -231,10 +231,11 @@ def close(self) -> List[Any]: self.step(None) except TypeError: # self.step -> self.worker.wait doesn't exist pass + for w in self.workers: + w.close() self.is_closed = True - return [w.close() for w in self.workers] - def __del__(self) -> List[Any]: + def __del__(self) -> None: if not self.is_closed: self.close() diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index a73de8769..27a7addd8 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -29,8 +29,8 @@ def reset(self) -> Any: def send_action(self, action: np.ndarray) -> None: pass - def get_result(self - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + def get_result( + self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: return self.result def step(self, action: np.ndarray @@ -49,7 +49,7 @@ def wait(workers: List['EnvWorker']) -> List['EnvWorker']: raise NotImplementedError @abstractmethod - def seed(self, seed: Optional[int] = None) -> int: + def seed(self, seed: Optional[int] = None) -> List[int]: pass @abstractmethod @@ -61,11 +61,8 @@ def render(self, **kwargs) -> Any: def close_env(self) -> Any: pass - def close(self) -> Any: + def close(self) -> None: if self.is_closed: return None self.is_closed = True - return self.close_env() - - def __del__(self) -> Any: - return self.close() + self.close_env() diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index 9f5770fc9..596cc52b0 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -21,15 +21,14 @@ def reset(self) -> Any: return self.env.reset() @staticmethod - def wait(workers: List['DummyEnvWorker'] - ) -> List['DummyEnvWorker']: + def wait(workers: List['DummyEnvWorker']) -> List['DummyEnvWorker']: # SequentialEnvWorker objects are always ready return workers def send_action(self, action: np.ndarray) -> None: self.result = self.env.step(action) - def seed(self, seed: Optional[int] = None) -> int: + def seed(self, seed: Optional[int] = None) -> List[int]: return self.env.seed(seed) if hasattr(self.env, 'seed') else None def render(self, **kwargs) -> Any: @@ -37,4 +36,4 @@ def render(self, **kwargs) -> Any: hasattr(self.env, 'render') else None def close_env(self) -> Any: - return self.env.close() + self.env.close() diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index 6e35adcbd..c59d86802 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -35,11 +35,11 @@ def send_action(self, action: np.ndarray) -> None: # self.action is actually a handle self.result = self.env.step.remote(action) - def get_result(self - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + def get_result( + self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: return ray.get(self.result) - def seed(self, seed: Optional[int] = None) -> int: + def seed(self, seed: Optional[int] = None) -> List[int]: if hasattr(self.env, 'seed'): return ray.get(self.env.seed.remote(seed)) return None @@ -50,4 +50,4 @@ def render(self, **kwargs) -> Any: return None def close_env(self) -> Any: - return ray.get(self.env.close.remote()) + ray.get(self.env.close.remote()) diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index 7e1634ade..684158d8c 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -6,12 +6,12 @@ from multiprocessing import Array, Pipe, connection from typing import Callable, Any, List, Tuple, Optional - from tianshou.env.worker import EnvWorker from tianshou.env.utils import CloudpickleWrapper def _worker(parent, p, env_fn_wrapper, obs_bufs=None): + def _encode_obs(obs, buffer): if isinstance(obs, np.ndarray): buffer.save(obs) @@ -22,6 +22,7 @@ def _encode_obs(obs, buffer): for k in obs.keys(): _encode_obs(obs[k], buffer[k]) return None + parent.close() env = env_fn_wrapper.data() try: @@ -133,6 +134,7 @@ def _setup_buf(space): return buffer def _decode_obs(self, isNone): + def decode_obs(buffer): if isinstance(buffer, ShArray): return buffer.get() @@ -142,6 +144,7 @@ def decode_obs(buffer): return {k: decode_obs(v) for k, v in buffer.items()} else: raise NotImplementedError + return decode_obs(self.buffer) def reset(self) -> Any: @@ -160,14 +163,14 @@ def wait(workers: List['SubprocEnvWorker']) -> List['SubprocEnvWorker']: def send_action(self, action: np.ndarray) -> None: self.parent_remote.send(['step', action]) - def get_result(self - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + def get_result( + self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: obs, rew, done, info = self.parent_remote.recv() if self.share_memory: obs = self._decode_obs(obs) return obs, rew, done, info - def seed(self, seed: Optional[int] = None) -> int: + def seed(self, seed: Optional[int] = None) -> List[int]: self.parent_remote.send(['seed', seed]) return self.parent_remote.recv() @@ -179,10 +182,9 @@ def close_env(self) -> Any: try: self.parent_remote.send(['close', None]) # mp may be deleted so it may raise AttributeError - result = self.parent_remote.recv() + self.parent_remote.recv() self.process.join() except (BrokenPipeError, EOFError, AttributeError): - result = None + pass # ensure the subproc is terminated self.process.terminate() - return result From 55bba25ebd2e5f58ae7a897c02c966a5fb8cfd57 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Mon, 10 Aug 2020 08:45:14 +0800 Subject: [PATCH 40/74] class assert --- tianshou/env/venvs.py | 8 +++++--- tianshou/env/worker/base.py | 2 +- tianshou/env/worker/dummy.py | 2 +- tianshou/env/worker/ray.py | 2 +- tianshou/env/worker/subproc.py | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index b9b97b039..cdffce4ce 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -61,9 +61,11 @@ def __init__(self, ) -> None: self._env_fns = env_fns self.workers = [worker_fn(fn) for fn in env_fns] - self.env_num = len(env_fns) - self.worker_class = self.workers[0].__class__ + self.worker_class = type(self.workers[0]) + assert issubclass(self.worker_class, EnvWorker) + assert all([isinstance(w, self.worker_class) for w in self.workers]) + self.env_num = len(env_fns) self.wait_num = wait_num or len(env_fns) assert 1 <= self.wait_num <= len(env_fns), \ f'wait_num should be in [1, {len(env_fns)}], but got {wait_num}' @@ -78,7 +80,7 @@ def __init__(self, self.ready_id = list(range(self.env_num)) self.is_closed = False - def _assert_is_not_closed(self): + def _assert_is_not_closed(self) -> None: assert not self.is_closed, f"Methods of {self.__class__.__name__} "\ "should not be called after close." diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 27a7addd8..d9c1e04c6 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -58,7 +58,7 @@ def render(self, **kwargs) -> Any: pass @abstractmethod - def close_env(self) -> Any: + def close_env(self) -> None: pass def close(self) -> None: diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index 596cc52b0..934a11082 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -35,5 +35,5 @@ def render(self, **kwargs) -> Any: return self.env.render(**kwargs) if \ hasattr(self.env, 'render') else None - def close_env(self) -> Any: + def close_env(self) -> None: self.env.close() diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index c59d86802..7e5917030 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -49,5 +49,5 @@ def render(self, **kwargs) -> Any: return ray.get(self.env.render.remote(**kwargs)) return None - def close_env(self) -> Any: + def close_env(self) -> None: ray.get(self.env.close.remote()) diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index 684158d8c..cfd045080 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -178,7 +178,7 @@ def render(self, **kwargs) -> Any: self.parent_remote.send(['render', kwargs]) return self.parent_remote.recv() - def close_env(self) -> Any: + def close_env(self) -> None: try: self.parent_remote.send(['close', None]) # mp may be deleted so it may raise AttributeError From ded97620408eb5454c81a206468c4c9d9e2b2c78 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Tue, 11 Aug 2020 15:06:08 +0800 Subject: [PATCH 41/74] pep8 --- tianshou/env/worker/base.py | 4 ++-- tianshou/env/worker/dummy.py | 4 ++-- tianshou/env/worker/ray.py | 10 ++++------ tianshou/env/worker/subproc.py | 10 ++++------ 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index d9c1e04c6..9d1af3d40 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -29,8 +29,8 @@ def reset(self) -> Any: def send_action(self, action: np.ndarray) -> None: pass - def get_result( - self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + def get_result(self) -> Tuple[ + np.ndarray, np.ndarray, np.ndarray, np.ndarray]: return self.result def step(self, action: np.ndarray diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index 934a11082..48d08f38e 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -32,8 +32,8 @@ def seed(self, seed: Optional[int] = None) -> List[int]: return self.env.seed(seed) if hasattr(self.env, 'seed') else None def render(self, **kwargs) -> Any: - return self.env.render(**kwargs) if \ - hasattr(self.env, 'render') else None + return self.env.render(**kwargs) \ + if hasattr(self.env, 'render') else None def close_env(self) -> None: self.env.close() diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index 7e5917030..d580e5943 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -25,18 +25,16 @@ def reset(self) -> Any: @staticmethod def wait(workers: List['RayEnvWorker']) -> List['RayEnvWorker']: - ready_envs, _ = ray.wait( - [x.env for x in workers], - num_returns=len(workers), - timeout=0) + ready_envs, _ = ray.wait([x.env for x in workers], + num_returns=len(workers), timeout=0) return [workers[ready_envs.index(env)] for env in ready_envs] def send_action(self, action: np.ndarray) -> None: # self.action is actually a handle self.result = self.env.step.remote(action) - def get_result( - self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + def get_result(self) -> Tuple[ + np.ndarray, np.ndarray, np.ndarray, np.ndarray]: return ray.get(self.result) def seed(self, seed: Optional[int] = None) -> List[int]: diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index cfd045080..7dfb61021 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -11,7 +11,6 @@ def _worker(parent, p, env_fn_wrapper, obs_bufs=None): - def _encode_obs(obs, buffer): if isinstance(obs, np.ndarray): buffer.save(obs) @@ -97,8 +96,8 @@ def get(self): class SubprocEnvWorker(EnvWorker): """Subprocess worker used in SubprocVectorEnv and ShmemVectorEnv.""" - def __init__(self, env_fn: Callable[[], gym.Env], share_memory=False - ) -> None: + def __init__(self, env_fn: Callable[[], gym.Env], + share_memory=False) -> None: super().__init__(env_fn) self.parent_remote, self.child_remote = Pipe() self.share_memory = share_memory @@ -134,7 +133,6 @@ def _setup_buf(space): return buffer def _decode_obs(self, isNone): - def decode_obs(buffer): if isinstance(buffer, ShArray): return buffer.get() @@ -163,8 +161,8 @@ def wait(workers: List['SubprocEnvWorker']) -> List['SubprocEnvWorker']: def send_action(self, action: np.ndarray) -> None: self.parent_remote.send(['step', action]) - def get_result( - self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + def get_result(self) -> Tuple[ + np.ndarray, np.ndarray, np.ndarray, np.ndarray]: obs, rew, done, info = self.parent_remote.recv() if self.share_memory: obs = self._decode_obs(obs) From 92a9e99cf7742dd4b34c8bdcd86a90617049c65f Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 12 Aug 2020 08:07:50 +0800 Subject: [PATCH 42/74] change examples structure --- examples/{ => atari}/atari.py | 0 examples/{ => atari}/pong_a2c.py | 0 examples/{ => atari}/pong_dqn.py | 0 examples/{ => atari}/pong_ppo.py | 0 examples/{ => box2d}/acrobot_dualdqn.py | 0 examples/{ => box2d}/bipedal_hardcore_sac.py | 0 examples/box2d/lunarlander_dqn.py | 109 ++++++++++++++++++ examples/{ => box2d}/sac_mcc.py | 0 examples/{ => mujoco}/README.md | 0 examples/{ => mujoco}/ant_v2_ddpg.py | 0 examples/{ => mujoco}/ant_v2_sac.py | 0 examples/{ => mujoco}/ant_v2_td3.py | 0 .../{ => mujoco}/halfcheetahBullet_v0_sac.py | 0 examples/mujoco/{ => mujoco}/__init__.py | 0 examples/mujoco/{ => mujoco}/assets/point.xml | 0 .../mujoco/{ => mujoco}/maze_env_utils.py | 0 examples/mujoco/{ => mujoco}/point.py | 0 .../mujoco/{ => mujoco}/point_maze_env.py | 0 examples/mujoco/{ => mujoco}/register.py | 0 examples/{ => mujoco}/point_maze_td3.py | 0 20 files changed, 109 insertions(+) rename examples/{ => atari}/atari.py (100%) rename examples/{ => atari}/pong_a2c.py (100%) rename examples/{ => atari}/pong_dqn.py (100%) rename examples/{ => atari}/pong_ppo.py (100%) rename examples/{ => box2d}/acrobot_dualdqn.py (100%) rename examples/{ => box2d}/bipedal_hardcore_sac.py (100%) create mode 100644 examples/box2d/lunarlander_dqn.py rename examples/{ => box2d}/sac_mcc.py (100%) rename examples/{ => mujoco}/README.md (100%) rename examples/{ => mujoco}/ant_v2_ddpg.py (100%) rename examples/{ => mujoco}/ant_v2_sac.py (100%) rename examples/{ => mujoco}/ant_v2_td3.py (100%) rename examples/{ => mujoco}/halfcheetahBullet_v0_sac.py (100%) rename examples/mujoco/{ => mujoco}/__init__.py (100%) rename examples/mujoco/{ => mujoco}/assets/point.xml (100%) rename examples/mujoco/{ => mujoco}/maze_env_utils.py (100%) rename examples/mujoco/{ => mujoco}/point.py (100%) rename examples/mujoco/{ => mujoco}/point_maze_env.py (100%) rename examples/mujoco/{ => mujoco}/register.py (100%) rename examples/{ => mujoco}/point_maze_td3.py (100%) diff --git a/examples/atari.py b/examples/atari/atari.py similarity index 100% rename from examples/atari.py rename to examples/atari/atari.py diff --git a/examples/pong_a2c.py b/examples/atari/pong_a2c.py similarity index 100% rename from examples/pong_a2c.py rename to examples/atari/pong_a2c.py diff --git a/examples/pong_dqn.py b/examples/atari/pong_dqn.py similarity index 100% rename from examples/pong_dqn.py rename to examples/atari/pong_dqn.py diff --git a/examples/pong_ppo.py b/examples/atari/pong_ppo.py similarity index 100% rename from examples/pong_ppo.py rename to examples/atari/pong_ppo.py diff --git a/examples/acrobot_dualdqn.py b/examples/box2d/acrobot_dualdqn.py similarity index 100% rename from examples/acrobot_dualdqn.py rename to examples/box2d/acrobot_dualdqn.py diff --git a/examples/bipedal_hardcore_sac.py b/examples/box2d/bipedal_hardcore_sac.py similarity index 100% rename from examples/bipedal_hardcore_sac.py rename to examples/box2d/bipedal_hardcore_sac.py diff --git a/examples/box2d/lunarlander_dqn.py b/examples/box2d/lunarlander_dqn.py new file mode 100644 index 000000000..72ef1ec55 --- /dev/null +++ b/examples/box2d/lunarlander_dqn.py @@ -0,0 +1,109 @@ +import os +import gym +import torch +import pprint +import argparse +import numpy as np +from torch.utils.tensorboard import SummaryWriter + +from tianshou.env import DummyVectorEnv, SubprocVectorEnv +from tianshou.policy import DQNPolicy +from tianshou.trainer import offpolicy_trainer +from tianshou.data import Collector, ReplayBuffer +from tianshou.utils.net.common import Net + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--task', type=str, default='LunarLander-v2') + parser.add_argument('--seed', type=int, default=0) + parser.add_argument('--eps-test', type=float, default=0.05) + parser.add_argument('--eps-train', type=float, default=1.0) + parser.add_argument('--buffer-size', type=int, default=100000) + parser.add_argument('--lr', type=float, default=1e-3) + parser.add_argument('--gamma', type=float, default=0.99) + parser.add_argument('--n-step', type=int, default=4) + parser.add_argument('--target-update-freq', type=int, default=500) + parser.add_argument('--epoch', type=int, default=10) + parser.add_argument('--step-per-epoch', type=int, default=10000) + parser.add_argument('--collect-per-step', type=int, default=16) + parser.add_argument('--batch-size', type=int, default=64) + parser.add_argument('--layer-num', type=int, default=3) + parser.add_argument('--training-num', type=int, default=10) + parser.add_argument('--test-num', type=int, default=100) + parser.add_argument('--logdir', type=str, default='log') + parser.add_argument('--render', type=float, default=0.) + parser.add_argument( + '--device', type=str, + default='cuda' if torch.cuda.is_available() else 'cpu') + args = parser.parse_known_args()[0] + return args + + +def test_dqn(args=get_args()): + env = gym.make(args.task) + args.state_shape = env.observation_space.shape or env.observation_space.n + args.action_shape = env.action_space.shape or env.action_space.n + # train_envs = gym.make(args.task) + # you can also use tianshou.env.SubprocVectorEnv + train_envs = DummyVectorEnv( + [lambda: gym.make(args.task) for _ in range(args.training_num)]) + # test_envs = gym.make(args.task) + test_envs = SubprocVectorEnv( + [lambda: gym.make(args.task) for _ in range(args.test_num)]) + # seed + np.random.seed(args.seed) + torch.manual_seed(args.seed) + train_envs.seed(args.seed) + test_envs.seed(args.seed) + # model + net = Net(args.layer_num, args.state_shape, + args.action_shape, args.device, + dueling=(2, 2)).to(args.device) + optim = torch.optim.Adam(net.parameters(), lr=args.lr) + policy = DQNPolicy( + net, optim, args.gamma, args.n_step, + target_update_freq=args.target_update_freq) + # collector + train_collector = Collector( + policy, train_envs, ReplayBuffer(args.buffer_size)) + test_collector = Collector(policy, test_envs) + # policy.set_eps(1) + train_collector.collect(n_step=args.batch_size) + # log + log_path = os.path.join(args.logdir, args.task, 'dqn') + writer = SummaryWriter(log_path) + + def save_fn(policy): + torch.save(policy.state_dict(), os.path.join(log_path, 'policy.pth')) + + def stop_fn(x): + return x >= env.spec.reward_threshold + + def train_fn(x): + args.eps_train = max(args.eps_train * 0.6, 0.01) + policy.set_eps(args.eps_train) + + def test_fn(x): + policy.set_eps(args.eps_test) + + # trainer + result = offpolicy_trainer( + policy, train_collector, test_collector, args.epoch, + args.step_per_epoch, args.collect_per_step, args.test_num, + args.batch_size, train_fn=train_fn, test_fn=test_fn, + stop_fn=stop_fn, save_fn=save_fn, writer=writer, + test_in_train=True) + + assert stop_fn(result['best_reward']) + if __name__ == '__main__': + pprint.pprint(result) + # Let's watch its performance! + env = gym.make(args.task) + collector = Collector(policy, env) + result = collector.collect(n_episode=1, render=args.render) + print(f'Final reward: {result["rew"]}, length: {result["len"]}') + + +if __name__ == '__main__': + test_dqn(get_args()) diff --git a/examples/sac_mcc.py b/examples/box2d/sac_mcc.py similarity index 100% rename from examples/sac_mcc.py rename to examples/box2d/sac_mcc.py diff --git a/examples/README.md b/examples/mujoco/README.md similarity index 100% rename from examples/README.md rename to examples/mujoco/README.md diff --git a/examples/ant_v2_ddpg.py b/examples/mujoco/ant_v2_ddpg.py similarity index 100% rename from examples/ant_v2_ddpg.py rename to examples/mujoco/ant_v2_ddpg.py diff --git a/examples/ant_v2_sac.py b/examples/mujoco/ant_v2_sac.py similarity index 100% rename from examples/ant_v2_sac.py rename to examples/mujoco/ant_v2_sac.py diff --git a/examples/ant_v2_td3.py b/examples/mujoco/ant_v2_td3.py similarity index 100% rename from examples/ant_v2_td3.py rename to examples/mujoco/ant_v2_td3.py diff --git a/examples/halfcheetahBullet_v0_sac.py b/examples/mujoco/halfcheetahBullet_v0_sac.py similarity index 100% rename from examples/halfcheetahBullet_v0_sac.py rename to examples/mujoco/halfcheetahBullet_v0_sac.py diff --git a/examples/mujoco/__init__.py b/examples/mujoco/mujoco/__init__.py similarity index 100% rename from examples/mujoco/__init__.py rename to examples/mujoco/mujoco/__init__.py diff --git a/examples/mujoco/assets/point.xml b/examples/mujoco/mujoco/assets/point.xml similarity index 100% rename from examples/mujoco/assets/point.xml rename to examples/mujoco/mujoco/assets/point.xml diff --git a/examples/mujoco/maze_env_utils.py b/examples/mujoco/mujoco/maze_env_utils.py similarity index 100% rename from examples/mujoco/maze_env_utils.py rename to examples/mujoco/mujoco/maze_env_utils.py diff --git a/examples/mujoco/point.py b/examples/mujoco/mujoco/point.py similarity index 100% rename from examples/mujoco/point.py rename to examples/mujoco/mujoco/point.py diff --git a/examples/mujoco/point_maze_env.py b/examples/mujoco/mujoco/point_maze_env.py similarity index 100% rename from examples/mujoco/point_maze_env.py rename to examples/mujoco/mujoco/point_maze_env.py diff --git a/examples/mujoco/register.py b/examples/mujoco/mujoco/register.py similarity index 100% rename from examples/mujoco/register.py rename to examples/mujoco/mujoco/register.py diff --git a/examples/point_maze_td3.py b/examples/mujoco/point_maze_td3.py similarity index 100% rename from examples/point_maze_td3.py rename to examples/mujoco/point_maze_td3.py From c3c89068752a9b5a927fad9c81eeba91eac164a4 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 12 Aug 2020 08:18:40 +0800 Subject: [PATCH 43/74] docs for Venv --- tianshou/env/venvs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index cdffce4ce..3830a6720 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -60,6 +60,8 @@ def __init__(self, wait_num: Optional[int] = None, ) -> None: self._env_fns = env_fns + # A VectorEnv contains a pool of EnvWorkers, which corresponds to + # interact with the given envs (one worker <-> one env). self.workers = [worker_fn(fn) for fn in env_fns] self.worker_class = type(self.workers[0]) assert issubclass(self.worker_class, EnvWorker) From 26f5b8ef6d04776e665650ca9a599c83509ccfdb Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 12 Aug 2020 10:53:18 +0800 Subject: [PATCH 44/74] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d1ceeaf45..e222ff917 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Here is Tianshou's other features: - Elegant framework, using only ~2000 lines of code -- Support parallel environment sampling for all algorithms [Usage](https://tianshou.readthedocs.io/en/latest/tutorials/cheatsheet.html#parallel-sampling) +- Support (asynchronous) parallel environment sampling for all algorithms [Usage](https://tianshou.readthedocs.io/en/latest/tutorials/cheatsheet.html#parallel-sampling) - Support recurrent state representation in actor network and critic network (RNN-style training for POMDP) [Usage](https://tianshou.readthedocs.io/en/latest/tutorials/cheatsheet.html#rnn-style-training) - Support any type of environment state (e.g. a dict, a self-defined class, ...) [Usage](https://tianshou.readthedocs.io/en/latest/tutorials/cheatsheet.html#user-defined-environment-and-different-state-representation) - Support customized training process [Usage](https://tianshou.readthedocs.io/en/latest/tutorials/cheatsheet.html#customize-training-process) From c3e43c2853343bf79280cd89ca53900134bd7420 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 12 Aug 2020 13:09:51 +0800 Subject: [PATCH 45/74] change ll_dqn --- examples/box2d/lunarlander_dqn.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/box2d/lunarlander_dqn.py b/examples/box2d/lunarlander_dqn.py index 72ef1ec55..c81082869 100644 --- a/examples/box2d/lunarlander_dqn.py +++ b/examples/box2d/lunarlander_dqn.py @@ -6,29 +6,30 @@ import numpy as np from torch.utils.tensorboard import SummaryWriter -from tianshou.env import DummyVectorEnv, SubprocVectorEnv from tianshou.policy import DQNPolicy +from tianshou.utils.net.common import Net from tianshou.trainer import offpolicy_trainer from tianshou.data import Collector, ReplayBuffer -from tianshou.utils.net.common import Net +from tianshou.env import DummyVectorEnv, SubprocVectorEnv def get_args(): parser = argparse.ArgumentParser() + # the parameters are found by Optuna parser.add_argument('--task', type=str, default='LunarLander-v2') parser.add_argument('--seed', type=int, default=0) parser.add_argument('--eps-test', type=float, default=0.05) - parser.add_argument('--eps-train', type=float, default=1.0) + parser.add_argument('--eps-train', type=float, default=0.73) parser.add_argument('--buffer-size', type=int, default=100000) - parser.add_argument('--lr', type=float, default=1e-3) + parser.add_argument('--lr', type=float, default=0.013) parser.add_argument('--gamma', type=float, default=0.99) parser.add_argument('--n-step', type=int, default=4) parser.add_argument('--target-update-freq', type=int, default=500) parser.add_argument('--epoch', type=int, default=10) - parser.add_argument('--step-per-epoch', type=int, default=10000) + parser.add_argument('--step-per-epoch', type=int, default=5000) parser.add_argument('--collect-per-step', type=int, default=16) - parser.add_argument('--batch-size', type=int, default=64) - parser.add_argument('--layer-num', type=int, default=3) + parser.add_argument('--batch-size', type=int, default=128) + parser.add_argument('--layer-num', type=int, default=1) parser.add_argument('--training-num', type=int, default=10) parser.add_argument('--test-num', type=int, default=100) parser.add_argument('--logdir', type=str, default='log') @@ -93,7 +94,7 @@ def test_fn(x): args.step_per_epoch, args.collect_per_step, args.test_num, args.batch_size, train_fn=train_fn, test_fn=test_fn, stop_fn=stop_fn, save_fn=save_fn, writer=writer, - test_in_train=True) + test_in_train=False) assert stop_fn(result['best_reward']) if __name__ == '__main__': From 3c254147f165a9e4e2f9ece9588450ead8be639d Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 12 Aug 2020 17:40:28 +0800 Subject: [PATCH 46/74] timeout --- test/base/test_env.py | 2 +- tianshou/env/venvs.py | 20 ++++++++++++++------ tianshou/env/worker/base.py | 3 ++- tianshou/env/worker/dummy.py | 3 ++- tianshou/env/worker/ray.py | 9 ++++++--- tianshou/env/worker/subproc.py | 8 +++++--- 6 files changed, 30 insertions(+), 15 deletions(-) diff --git a/test/base/test_env.py b/test/base/test_env.py index 06296df58..ba1eceb95 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -36,7 +36,7 @@ def test_async_env(num=8, sleep=0.1): lambda i=i: MyTestEnv(size=i, sleep=sleep, random_sleep=True) for i in range(size, size + num) ] - v = SubprocVectorEnv(env_fns, wait_num=num // 2) + v = SubprocVectorEnv(env_fns, wait_num=num // 2, timeout=1e-3) v.seed() v.reset() # for a random variable u ~ U[0, 1], let v = max{u1, u2, ..., un} diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 3830a6720..6ace0e188 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -52,12 +52,14 @@ def seed(self, seed): return when ``wait_num`` environments finish a step and keep on simulation in these environments. If ``None``, asynchronous simulation is disabled; else, ``1 <= wait_num <= env_num``. + :param float timeout: TODO """ def __init__(self, env_fns: List[Callable[[], gym.Env]], worker_fn: Callable[[Callable[[], gym.Env]], EnvWorker], wait_num: Optional[int] = None, + timeout: Optional[float] = None, ) -> None: self._env_fns = env_fns # A VectorEnv contains a pool of EnvWorkers, which corresponds to @@ -71,7 +73,8 @@ def __init__(self, self.wait_num = wait_num or len(env_fns) assert 1 <= self.wait_num <= len(env_fns), \ f'wait_num should be in [1, {len(env_fns)}], but got {wait_num}' - self.is_async = self.wait_num != len(env_fns) + self.timeout = timeout + self.is_async = self.wait_num != len(env_fns) or timeout is not None self.waiting_conn = [] # environments in self.ready_id is actually ready # but environments in self.waiting_id are just waiting when checked, @@ -184,7 +187,8 @@ def step(self, self.ready_id = [x for x in self.ready_id if x not in id] result = [] while len(self.waiting_conn) > 0 and len(result) < self.wait_num: - ready_conns = self.worker_class.wait(self.waiting_conn) + ready_conns = self.worker_class.wait( + self.waiting_conn, self.timeout) for conn in ready_conns: waiting_index = self.waiting_conn.index(conn) self.waiting_conn.pop(waiting_index) @@ -254,7 +258,8 @@ class DummyVectorEnv(BaseVectorEnv): """ def __init__(self, env_fns: List[Callable[[], gym.Env]], - wait_num: Optional[int] = None) -> None: + wait_num: Optional[int] = None, + timeout: Optional[float] = None) -> None: super().__init__(env_fns, DummyEnvWorker, wait_num=wait_num) @@ -276,7 +281,8 @@ class SubprocVectorEnv(BaseVectorEnv): """ def __init__(self, env_fns: List[Callable[[], gym.Env]], - wait_num: Optional[int] = None) -> None: + wait_num: Optional[int] = None, + timeout: Optional[float] = None) -> None: def worker_fn(fn): return SubprocEnvWorker(fn, share_memory=False) super().__init__(env_fns, worker_fn, wait_num=wait_num) @@ -294,7 +300,8 @@ class ShmemVectorEnv(BaseVectorEnv): """ def __init__(self, env_fns: List[Callable[[], gym.Env]], - wait_num: Optional[int] = None) -> None: + wait_num: Optional[int] = None, + timeout: Optional[float] = None) -> None: def worker_fn(fn): return SubprocEnvWorker(fn, share_memory=True) super().__init__(env_fns, worker_fn, wait_num=wait_num) @@ -312,7 +319,8 @@ class RayVectorEnv(BaseVectorEnv): """ def __init__(self, env_fns: List[Callable[[], gym.Env]], - wait_num: Optional[int] = None) -> None: + wait_num: Optional[int] = None, + timeout: Optional[float] = None) -> None: try: import ray except ImportError as e: diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 9d1af3d40..4dece8bdc 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -44,7 +44,8 @@ def step(self, action: np.ndarray return self.get_result() @staticmethod - def wait(workers: List['EnvWorker']) -> List['EnvWorker']: + def wait(workers: List['EnvWorker'], + timeout: Optional[float] = None) -> List['EnvWorker']: """Given a list of workers, return those ready ones.""" raise NotImplementedError diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index 48d08f38e..8d6e4e08c 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -21,7 +21,8 @@ def reset(self) -> Any: return self.env.reset() @staticmethod - def wait(workers: List['DummyEnvWorker']) -> List['DummyEnvWorker']: + def wait(workers: List['DummyEnvWorker'], + timeout: Optional[float] = None) -> List['DummyEnvWorker']: # SequentialEnvWorker objects are always ready return workers diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index d580e5943..be972bfa2 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -24,9 +24,12 @@ def reset(self) -> Any: return ray.get(self.env.reset.remote()) @staticmethod - def wait(workers: List['RayEnvWorker']) -> List['RayEnvWorker']: - ready_envs, _ = ray.wait([x.env for x in workers], - num_returns=len(workers), timeout=0) + def wait(workers: List['RayEnvWorker'], + timeout: Optional[float] = None) -> List['RayEnvWorker']: + ready_envs = [] + while not ready_envs: + ready_envs, _ = ray.wait([x.env for x in workers], + num_returns=len(workers), timeout=timeout) return [workers[ready_envs.index(env)] for env in ready_envs] def send_action(self, action: np.ndarray) -> None: diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index 7dfb61021..0999ff22b 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -153,9 +153,11 @@ def reset(self) -> Any: return obs @staticmethod - def wait(workers: List['SubprocEnvWorker']) -> List['SubprocEnvWorker']: - conns = [x.parent_remote for x in workers] - ready_conns = connection.wait(conns) + def wait(workers: List['SubprocEnvWorker'], + timeout: Optional[float] = None) -> List['SubprocEnvWorker']: + conns, ready_conns = [x.parent_remote for x in workers], [] + while not ready_conns: + ready_conns = connection.wait(conns, timeout=timeout) return [workers[conns.index(con)] for con in ready_conns] def send_action(self, action: np.ndarray) -> None: From ba9b6c70d5d07e0ecede9691891e6ecde8b32992 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 12 Aug 2020 18:04:28 +0800 Subject: [PATCH 47/74] timeout docs --- tianshou/env/venvs.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 6ace0e188..7ccac9ef5 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -42,17 +42,19 @@ def seed(self, seed): Otherwise, the outputs of these envs may be the same with each other. - :param env_fns: a list of callable envs, ``env_fns[i]()`` will generate - the ith env. - :param worker_fn: a callable worker, ``worker_fn(env_fns[i])`` will - generate a worker which contains this env. - :param int wait_num: used in asynchronous simulation if the time cost of + :param env_fns: a list of callable envs, ``env_fns[i]()`` generates the ith + env. + :param worker_fn: a callable worker, ``worker_fn(env_fns[i])`` generates a + worker which contains this env. + :param int wait_num: use in asynchronous simulation if the time cost of ``env.step`` varies with time and synchronously waiting for all environments to finish a step is time-wasting. In that case, we can return when ``wait_num`` environments finish a step and keep on simulation in these environments. If ``None``, asynchronous simulation is disabled; else, ``1 <= wait_num <= env_num``. - :param float timeout: TODO + :param float timeout: use in asynchronous simulation same as above, in each + vectorized step it only deal with those environments spending time + within the timeout. """ def __init__(self, From 17aeacddc4661b4625e4f2c508bc61ed99b6f9ca Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Thu, 13 Aug 2020 08:39:10 +0800 Subject: [PATCH 48/74] add a figure in cheatsheet --- docs/_static/images/async.png | Bin 0 -> 37183 bytes docs/tutorials/cheatsheet.rst | 8 +++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 docs/_static/images/async.png diff --git a/docs/_static/images/async.png b/docs/_static/images/async.png new file mode 100644 index 0000000000000000000000000000000000000000..6a88750b9ee25d831e364bef0ebdc3d17d9a783f GIT binary patch literal 37183 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 EFJ6w5*Z=?k literal 0 HcmV?d00001 diff --git a/docs/tutorials/cheatsheet.rst b/docs/tutorials/cheatsheet.rst index 71aa68d3a..0c99f896c 100644 --- a/docs/tutorials/cheatsheet.rst +++ b/docs/tutorials/cheatsheet.rst @@ -48,7 +48,13 @@ where ``env_fns`` is a list of callable env hooker. The above code can be writte env_fns = [lambda x=i: MyTestEnv(size=x) for i in [2, 3, 4, 5]] venv = SubprocVectorEnv(env_fns) -All subclasses of :class:`~tianshou.env.BaseVectorEnv` have an async mode (related to issue #103), where we can give it an extra parameter ``wait_num``. If we have 4 envs and set ``wait_num = 3``, each of the step in VectorEnv only returns 3 results of these 4 envs. This mode eases the case where each step cost varies at different timescale, e.g. 90% step cost 1s, but 10% cost 10s. +All subclasses of :class:`~tianshou.env.BaseVectorEnv` have an async mode (related to `Issue 103 `_), where we can give it two extra parameters ``wait_num`` or ``timeout`` (or both). If we have 4 envs and set ``wait_num = 3``, each of the step in VectorEnv only returns 3 results of these 4 envs. This mode eases the case where each step cost varies at different timescale, e.g. 90% step cost 1s, but 10% cost 10s. + +.. sidebar:: An example of async VectorEnv + + .. Figure:: ../_static/images/async.png + +You can treat the ``timeout`` parameter as a dynamic ``wait_num``. In each vectorized step it only returns the environments finished within the given time. If there is no such environment, it will wait until any of them finished. .. warning:: From f78b13322f55ca5b73d9e073d19c30d4eeefa1fe Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Thu, 13 Aug 2020 09:37:30 +0800 Subject: [PATCH 49/74] change image --- docs/_static/images/async.png | Bin 37183 -> 36743 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/_static/images/async.png b/docs/_static/images/async.png index 6a88750b9ee25d831e364bef0ebdc3d17d9a783f..e305044cf959f91b2a56b1379b5db2568c619500 100644 GIT binary patch literal 36743 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
  • AmIt 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, where we can give it two extra parameters ``wait_num`` or ``timeout`` (or both). If we have 4 envs and set ``wait_num = 3``, each of the step in VectorEnv only returns 3 results of these 4 envs. This mode eases the case where each step cost varies at different timescale, e.g. 90% step cost 1s, but 10% cost 10s. - -.. sidebar:: An example of async VectorEnv +.. sidebar:: An example of sync/async VectorEnv (the same color represents the same batch forward) .. Figure:: ../_static/images/async.png +All subclasses of :class:`~tianshou.env.BaseVectorEnv` have an async mode (related to `Issue 103 `_), where we can give it two extra parameters ``wait_num`` or ``timeout`` (or both). If we have 4 envs and set ``wait_num = 3``, each of the step in VectorEnv only returns 3 results of these 4 envs. This mode eases the case where each step cost varies at different timescale, e.g. 90% step cost 1s, but 10% cost 10s. + You can treat the ``timeout`` parameter as a dynamic ``wait_num``. In each vectorized step it only returns the environments finished within the given time. If there is no such environment, it will wait until any of them finished. .. warning:: diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 7ccac9ef5..867067ebf 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -54,7 +54,7 @@ def seed(self, seed): is disabled; else, ``1 <= wait_num <= env_num``. :param float timeout: use in asynchronous simulation same as above, in each vectorized step it only deal with those environments spending time - within the timeout. + within ``timeout`` seconds. """ def __init__(self, From 1b3d323d697a5d9865ef536a3d616a499424258e Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Thu, 13 Aug 2020 18:05:33 +0800 Subject: [PATCH 51/74] change image color --- docs/_static/images/async.png | Bin 54626 -> 54780 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/_static/images/async.png b/docs/_static/images/async.png index 9c86a840b7a791d2969222fd9acf9b7108f088b3..c61b2c4f231f4c0a8d34c5fa31047bcc677f0463 100644 GIT binary patch literal 54780 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 literal 54626 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%0DA;TSYqmPFA^r$Jh2s40(xuHaOkWq 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 From 025129ed32804b3de72fbba99d4e6888e3cd0e40 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Fri, 14 Aug 2020 07:43:24 +0800 Subject: [PATCH 52/74] change title --- docs/_static/images/async.png | Bin 54780 -> 56551 bytes docs/tutorials/cheatsheet.rst | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_static/images/async.png b/docs/_static/images/async.png index c61b2c4f231f4c0a8d34c5fa31047bcc677f0463..11547dedca5df6737b4ad720ab0605aa6f85a1d2 100644 GIT binary patch literal 56551 zcmeFabzIhY*DZ{NE#p{#3XY-(D2jp-0w#z^NJ^`SG)RY(iGd<0U=SkRigcQ&AR#TO z(jcIOG`wr$%=^6Oo@dVeobx&H&pV&beLId9*Y&%;vG>|*t-ZY_FDtot0qX(=28P8_ zhYu<+FwC;V|HkJr;=jx?^^n3Zv#s_^Db2$_PV-J*#qU|H4;{BwG(Tf)chXXiLEp^W zRBx-5uBD!ynU#ULb?>ZPF$RWp3{nU8DcSpft3PMIe6W6Ms_H|}89S?)FS*Vi_FBh( z;mB{xXNRtr6ne)M<(PW?rB>Yay1;GGD|?M9R18{FdTX!qS3Xu=+PrT4yhyk8>Q81| z_uqGLzu<%0O`ARy_cG;tV&|JbyZZFbJ||7p_=19w4^9m`MecmKQT$DRi>}8~JS4+& zzHf(D(Z4VV?wl<|zn*2jux9%Ai}o#{U)Nk&H9@~M z&cjkLFnr$rKl$TzUc3xNrM)@#y?AFVN&G6;v_b2ogQxM&0YSNA=ey?YO)G!fxKiTZ zeAb!>HaTY2;p*a5{ePB72X@Ai|n=h3c}pkU5-zT4#r%}Umuc{r5WVZ zcdfyBNXxV_N9)B2`R>}(s=+Fqgvp7qf)>j|83$<@mK=-NXV%|ZsGj3A!jjPS!``}J zYijvEWiN|?_G9%9Z2^vL0d`8-+H9j!>@LH>+Abq{i`NP(ZOa``R8KKsdBII9}g;j#ebB*W|++xm~&S zUba(y-@fx_KHLjVOx$79QF`dkCYk5~Ey2{lf4y{{S#hAKQAM=RxJ&NJt?Hoy#+8w(3EG_{!56PA zVtKspB4gA6_a#T9rOO^4y1_js*r4_n|hS`IBf0x4ZJ$uXyNb(NKrLa{A<{$q3S(cha|udcI8+$im9Q2pej z`m-}P_po{XzHHfw6)VC{Y>$kM-BNGgbkj%MVf%^a3>!H)d48K`P~ZxY+(I>WV4l0h=Wm;2S9j{m5d;P`@#qG@HDWIWzf+lC@8_OEIcYL?YQ^dyLUse z08-v!lf$|=oe4Fj37Y8&_?AA!ZHf{SS4_t0mdM~U&d-{A+T&PL-pv*4>^i!&!^8G( z^74*&Cq8>NfBG)VMrmKrG&75L9O{XC{rYv;B*(^$hWU57Dz32TdSKyq?A$5fGCo|= zwdnWXf9KcBU%q&apuBg|$ybbP1<&p+y{5_%8WB;@*5C2`{H%(cs`#WNC9BpCbSQ*v zzeNU$o(t?3^Yim_-^BaFG21fJIB*@l_Q-?_i|*Z^Af2YjKWMWL4=!)M@Z!-DcClE0 ze}DCfa(=52JvHpCVq=bT%$9z~?2OLnnA2*sa5mqnpOz%{)Mv(6wH0ym@RZf1TdkNk zn>#L3znCpfJ^6^Qo?-JlFQf9vk3zlA_$0!@!n{KxBO{MIQ&vgUId<%rcPNhXblh`O zjP@IPhr>|s*cEr=@L@qyje^Ae=VOHfgl)Ww+hT0h%EDzfDla)!dQEZ_?q{4$SEXTh zO>#}TRfK|~;(-D`Uae>x!6=KSJXQgt^84Rv42KR0FFh?cly>@%y;ZuVK##~@-w$iK zN#?FOCG7rN;Bt1JPh8mgXq(6SVzA!*U6){KDabRAp zbb)E@k~u}Cr3yZNepS`Gm={Ikl=$ksZ~EciKh#feKAtAT%je&HRe9`ZcSVF8`<#pW zZ~=5KU%u=eiaT$NsAid2pa1UN?W(TD8UZp7_`UShxVLPH?GtQ!k2O}7mtWWMI&%Gn zU+3YC#l9YsLx&D=ZP;+YELB5IO|2%)BE*AF{mF^*eSLj*{rzKS%woJ0Cti?P9xk(5 z%{MG7Ymf8P#E?J=o;b3vzkefF(sVW{Z_b);>1|yoP!(xfo04B#T=n(M4WEY(V}=^e z$>lLGX4`XIN=oWvcLFxjGF_9Ko4fSZS`j0YRJ&g7mX?UOh6|BTr3X6zv7u{>qQ>TXStGe#Dl6)T?W2dl}oIy^H_pW=C zcXM@DU%@se1-;>-1|=@@}n-xp$8l0ZFPXLQeJAF)v}eo>;T`jLX@%&CSgv zF~6TF4vfRqQZ?)^+}+rY@bAYbk$rBc^71Ne=AXj9mcG0o)@S3!jgH;PRpnU@{pty2 z(aJnGS$4MG?bQ)kt(9SYOkMqfN}PHaZvFcfOT!QM_;7|78jF`+Q{(59^^bgWV^tTz zcg5D!y2q+I&I0^9TjRAdt-InZGsry~#pbR9)>$MpwLIf$vs5HD8Jj zH+VB5i$)VoHSFqb{LU0U2*YMYBNhd@PNrAJs)gX>B5`frxlVNaWBvT3c#*if&yX(sNBVO^R2cpM+`P?VVqzbBDCIKji&uZ9UtD=<{xW1|7nLpz-!RTV z`~9AK`VQaYnFc-=G?Njx;}ATtn1L0ezIt5P$ykD~33Y>HFkBEFeICGt5XOsEDI|1}z{|ufsiA+C@`^kgKIV zGR@C~O7GnoJ6tw=*w2meg7HfIPN&I<7lRje+KB0FySnFp6podYbTP=b+_G>0{E|+{ zQ*=MjxmQoFbb!BMsijIGO z)mfFGvsS5c`)PgsE&w9=XJ-lrUM}4Bu6IZ>gM$|NpP<;{15#4g=x}eI-dHM-^|CMNQ` zPL3b;qXm0@ehuIHROGp2qlzOgxjHviZY{-!m&d4XvuQ8cci_sRkB|0UEDt^~7jI_x z@$n&_oO8bS?{j&+-4Il4m;V{ZJsx0EF~u|ncdRDU&iuI1Hl$G8D^zvjS_=HS zK1#CtAXOCnq(}C5U9+@VHs$pD-xsgmb=0^r)(dNP>J)*L<%<}7e0;)In`b>O!QMul_wV1IjH*g89qcHR?9CaAC;ZBj zkhNn=E#2$0la@-idAPVf-q@CdWWBlY9^V1UTWgNl+NJ=i2sreecF5LlX=^hEe&pum z^#w}VXjt-SpLeLI_~db9z8KqD9@Y5rhU{}K{Udk1y#AXq5&>?o7z>FGTXv5TL1es7q~ z(6>(^$}uYM6Sv%4BcyPXRbWBybmVZ`9_^>b<17|rmOZGlk%PlOB~DK*c&|!;Y=H3l zwtnTzSinVZS&uqA;+08PC#Nil{|bnie)#fC$N82xV!-k&y?HgRDCggAPAafH6Tf|raAk#k?)R}?by0HP2|jTJMs*^ ze|~WUcg*_QpD$LvZQPKRer3s8Ze0(t@$U!Xw6oPUQcP&m9UVv2L=)%J3kSQi7Fed3 z8Utuvzjo~-@EO1Js5Q`k6o5)+Wt>K60VN4H^(d^q^pPV2HO2{14*jjk+7Z|#N@AM4 zs>h8xO2clHmX;5X-%hdyJzWbCChiXXg%$dOc6vUdCiLs%elzJ3g{AP7&Whn@BQ58QsufJ{j zs9~NRmfGORb4q}I_-uT?<*#hy@9`%Upt$W$fS?SDK%nz2x*ycy%+-7!@EblJ9vsW&EW0h+j!&P|^ncz&JgO`uML z=Wa8+c&9UT^qbS1^hf`}Mf_h*r~ksFB7E^S6JRU!)du<6Nb&>)7suYxau;DrN}2A&w(C7aWJD{o?YE+q(?r36v|j zbBNyWKOw5RUbLp~lVk5PxBl(_l3k;oN%GwMr>we?WzL*A>Soiv{}mscYM0`pC;XjZl1c4m&z2R(b%37Z}Zn=2Y0k0>S z*6zZcyL)$evT>EHuil}I*xRm?{Xq+6&%A!~W(8n?hHhrTwp`8m^XKQkfBzAc|7j0H z`RPFL=%mH#t97W9%TYR@n%SzJbQpQ|Sb}yoW%9)=J2(aePoaze2cl%S-|N3XbCjVF zsMIhwM3goH+PK*bq>pU9Q7WbTswZv4-q}LD0$bD)55BsxxC{uQ`G@`8d-o{uB3~i( zbmXlzj~&Rrh#5%>Jwy(sGE&k1K2w*P*j{b?R}FdiYH5r1RwW7$+m&0n?&06Qno z*0?IZ0t>8Ho8mHB7PNzvV9V#bYgRNP5D$F4u?AD}wg2pA^C%5QL+qoc`FyfVjmEV}+DO1fm!g-E+d7Ui|?s__N`NIb<2fBWR^u7mtSY2$FJ!zZ>@@tu($dms&}w}6 zhf%7onlI8pd8&CMcL7SS_>4|&6whD3o?0tnyCo?W)J^!oSEm3boG3C zCK;m==dGv4Dr6Z7hGBWbEcu&wAkoR;CdZK$KRu6+ZZl?CqxkMWPPLC)+5Z+mP@{&E zUkVax5E~c`c2xA-&+-rVcgl^3PM5)v^4szi6A+6EKmwy9Us>eiZzunse!T}4+#mg&;J1Fny=vX}+g3h}9 z6O=hxEg7F--SIdniJ!PNJQW>xEa>u8t5#LIa2TTUAH_A?D79ADx&%RNpw6neqPhXu zJ3Rb~w#jtqR1xL3PWAZl$O!Nj9+e#xF@Csh;qC41PbxvHh5$`Fk9I_WKLrO@^8L%J zS2gY&g!S>>-~jwM;xtlaanVFtgnjio{Bp;`P-{HD@q$oPd3%;)QsPUSDy<0Lts41> zTfkyytkSYRRy`(Y-F`b~;%Ds>^J&qe)8t@LiYXuw_s*UD{;3ryrW)Oixl>8qpobzr zMg*j2AwQtJcDw=5Pj%&OdifC|B2^rhyA8Yfr?X!RNVYs(aGDPFY*@h6GG#9=3igsf3>Rf zuf)Q?AfbQNlq@7O7zCwr$IeJ(%w`xqrv4Xu(kLjnG*_fZ=P!}wpERdUA}@rDZ7hl% zyUnp&OB2ouM}`Df$t$j1(ZlSg>gjWMTQ)2E4^~#kwFdVBKR7&T7<0n9mG4`d96zIN z+h24B9S7!+D8&e)_ud=zv*K~8K`O?&OpI3S)ARBI)h=Y!+8*L5R*GD=V%4e$)KrG3 z2r`sZbAl{d9VqP zp}g32(yFDo8H$O-sOQu$(_s6@1AuG~cAE#{$cGa&N>5LB1W9S^>_HptpZj-AL{btk zQQ3=&b1NPnT9}P{K^3UM2r}1nB6A&YVoyeuPvmp%@SAPxkCOz%zb`XmIkT~zTC~#yM5m_3AMS<$Aea1|1Q7;r-L|F zgrW^Vysuv$Bx)Q4D-VzK?go3^Fjcli%-i>Z+yK;juv7omg%>B1Yent9Kl0_(;+E=c z%+2+_bEnHm$AizY>E`Yvt2SBGyNlVyG|f^&=1NV6uvwCG(gA9-hrixH;alp#veUZx z-BLY$eI5}JjXRujzExdR+LBHHv>>IRpy188n3Y3nbhybg*}O5wC^g%%Wh>56v>9nD%^kLsceV-7a5mT_$5wNtKfuyC*7qoE(DL2*Yw?cc3Y8 zDTXUK?n%7BCxO}@s|on-WDq@h>C$YVs6&86cdbu(d;M2=5RsK~Vk4ozm*GJZ-}LE- zBE!+CQG0fVYuB!nqER|7-|5;`w3iIw;ZSy^Kjstl*45dXqscLEM`=Jea{>b-9KUqV(R zcL7~{GJC~9H$e$Kx^NY5ti>4_>>|VMHO$RLDoRRsckI{!yhPL>E>u2q$ee{MH*eb} zhdaW${lwmbM#i{{>d8hMbv=IeHnKmkm`+^LRt>X-q%v9`7(*Y_pzQzS>zn+-!V2)K zoVp&2Ol+}$gii)>fJp7e3F(6JjT*0qt5(J1<);*9l?}J(F^bsR{<}1fWA&`^RXR?= zz)*#Oq8d<~W1rST`bc)2aseK6{Rx@<_jsV=V>nP3=Q8<1as@#$**i6v zkZ;Tsx@?00RRE0+^*R*qhcXu#vFpt$o!B319cabnzhpUG>HW{dqKwZM7AD#M zI3+s#`62{auUl-QxS%Q>AbI5>xRJy{*MpJ=PH8{|g~QKpLy9DtL5_olh4@_TpaE*H z0v{h^u)uow`$-nvBI2&fL#39zdHdEq zeFwdC-8*GtB%*&Hx9AoB1nGXUS1)$xkBzS@lK#a8F?^OZd=r!>nQ^I~&9&+C!SNnJ zSH4fvPa^nFKAe+(p}W7}%l{ze`d{qh`TsxtKWN8)gLbj<;`@OaP{?>%vn;hiX0LY{ z>qcQ$g);Y;Zf57L3V<>jf^RVQAXlnr`7LnQ03nAs3c6Mh*zaJ5KV!_UDvsPt*1#1! zDpXCXtGTU&4)tZmcv!6*8!{YmEVXIh2Q1ZaMyFd>mR+*@=M82hDb>uae9g#p<1h>d9~< zHrl?y=kDEuP~14_Tdk+u4xuCgY7TE{7{C1*o=0CkJDS+fJnzQl(7e?2Q{u+bFowwU zBBgT}<}vAdz&#M{u_qt_&lHRZ2HQ*|9xHJRPm+i3obucU@lx?`uXI3XuL3Fkrfnww z326V~N07R~H+WdHFif(?OSKITn|+c`2}an0oL5i-CFJ-nz8{)oK#50|G)yD1h^p(cn!0n!<0G~- z3DzkDS;Q~sVNnVXL3IEj8-b67Kdf@2pXTFBmoAl=i7+oR0u-uwrVml&L}ZVmb1P>- zK|vWF7O*!0Qk+d^c~oVWieQ;>{2vQv&#cL?iOCu35y2jA1_3~pwmGXbMsq1>nn1$0faJY|h!0i`*DF#l;(s-rw;i$>dY% zabG;R#pBK zg!1M?7UUU*l%4sWcX1?8vXJy!RNPxXt31Ibjr4Svj<+h`8K$^=sA$@r|k z{3AAi3s$wJsrT_MS#>?so}RiaB`qCO?h12<1Ke~mqUJaG!qd{K_e_oVhihfpD&Mk0 zE%Tw*b?V$5KR;F7fVoU;{C17bsn>4Yh_TLsNvRMi(RdQTlC?HP1}FCePFQrTYf4q! zr^uMDj%KSn!OWD;_^?KvDo0)2CYs_zg>6 zTLYD8x{S1hqX-V3YQ%jgDV)@q>{~^T0(Tx>mZr(OY}qnhxcLf!2KMjXgy`1_q`J#2 zwf681HqW=A;o%QZDbi1DDcAu49-11R9;N7*uV*;8x${q-J`EEkbCYHIhcG{FBVZ!4 znpAUhY&Z@aj@UDNOZDD<`dHoClxsI{3SjM{6UPCg1fD*98vG>9UGTNPun zd-v`te2q(HTVTgI3fY20$ZNN5?eg}(Atr0*QEXS-tvBy?r;+>fny2S(Up*F_Lj>ie z86RQKse>1;759EoKm_3XE(jLEju3E?cTc)6S-S&nx5dfu zM}F3t$CRhRn@|Scw4{xo8U9W6j=#SNRuIKkD?C^UeD4AZ1ng2ezdA-gLG6S;T_bFU z;(a?Jz-}Dq#R0kAv~nmteNxow1=MOAwc5UF7EgiWzX#~Mx;CY-JWA2{n?8bDaQ_Jq zlD`fe&QkpH1;+hNwwZNJ(z<1^{DC+ zKmt1*_V6p5s}EtkC{C+4qrC@BvrK*EBE~|9$Qn)Tb$xx7{g5)5{R;H(ML@S?e@eH~ zH*kQwG*W2pqKks)DVEK))^yWv^Dg~!dBE}0zYZEOSsaydm!TieW~|6wB{_f6B*gxuxIJb$kPVw4aZU9%|&}3r^4mBRLANI}dnqZQHgRTK$<% ziF)Ax?-JgyA;^S%{EWiK3Kr>0GhfQ=u2SVhBJktYI=grTuIL+G@0oQ0CyoY*IeX}-g+qLA#R1rb;y&00 zdHcgaN12R?Ni4#Gb#FsG1i>r5Rn{_rJ&t=++Z-p|dEQl2IOJG&D9{PD`lRPqF&cpt zJM-n`WuMOC_wV`YGi-M9`W4h6l!nK~LJH?Uivu)cKI6qRxP6pY-f#4~#P&=Gs}|Vt zF~l98v}pCNE!ItT84rUIo*sZHLil14JbNP9(;Kz${r7h`X+t$N{ngSf^$b3}|B~0% ze;*uDNd71mtM$iQ_TBi%;fYCgaogjFfTM$56sS;N#t+zQ!ym!|9_p#7$iTJ^aT7%) zrPE-+DVh*lcNE-P^#x566O-gIi@pseM29UmF|ySGw+q%5(!>2v*gpfc(2I4S0;j$#u(YuWbUy$nc3;rqF+NBW9`9gD z%lVdjU*(-^c4G|xTJRIKEesJFJ}roYBFBH?ia3iH#@6N0J|8)btL;zyS_^ z00e`Qt*x!WCs0EJ39b{;e0+Qv-6^%rmg(StH?7@czrDN_K}lIf#TRNfob8sf%WjXK z)qx@1a(vn>wpwC^h9uo6P#rgsIVOaJ6v%Hm^M{wLt;)a5#!lI(%J+}YxrYp=gRLUWN}K(7wRD~xk&tE4L>fa%O^)a z#oCZ@2>1g2rqPnA;{5zl)OaL0-DKw5kN8{)^%|`D75Rt7;!~Prbc2--HogS)K~)W~ zQ%|$B&65kDhjSMsLb#6+l@+&+4@&rD?T0Y|7c`vuNJ{(A6k&ie09qldh^+7r1ltbDfvV7{m^I1&g_Accd0s_h|xw$pY z@%I+*Ldii|uwHCmO6J_101@~66QS;u5$WmRBF@ZcLA?MfgY zBps`mgdA@^2`pZjoJ-#wqpHCXr{KM=iDz}J^rLy>BjAoenAdXrdW{n{mBfS_j)M@g0YPSL4XTd|FT-x{(t$g+%T5-2X`$<&fL@^*tQE7W`cY$LT7@-(CF2M7`xtUE={2<}2x6a~WEFfnonq=fnAfT) z-Uuzp5SqSYi;wES7k74UPEJXHqEVc%B&QKegcMv%ZWvi=Cbf90Si0pvkAkQSUFI2d zd2CqaY-W8#T-VV~ULKx>H>3TzUn`rWe)Z>tPaSO%VH5FYvs+G6(lA=sCSbR#v(vFA z$2oJdD|b>_+ILHor74s>+?8WLe*93DxUu^BpJzRtBMz-Qq$b6=R;iTpkpcMbT?5d= zbgM!6sw8M@g>{4E-5`a&jdDqL=TIolk-%da<9!A5o%dc^itP5TYtZQBrTIh**?y}I zruHQOF!gjxGU|sT6l4SB3R^Z`MRhITBy(;KdR-cmJ5eA&+)#<}lO3Cwdb zpB?~mR76b~J<%h5z#2%TphdyaF@rh>;6X5K$@f7|N(mCY4}3uQJ_fI^E2ri$=;s8;m{XCr2GGPU$X*Jbvg=jMTc{B7bIve#vMplF-tWBhBZ@$yZ6v zW472lT=~POoRUcyLcvD;YN#8^A-d{M^aoAtX>&gNVCQ^=#rxibyk0TYZUN&=P~rqk z6;Q=?OL@m<|5V?qot0ME_>u8fzNpG^J_m_So7`=12Y~a~L3X8wyG%<_u_ikX8foE= zBI+a{dq6|mDD~#en~Z@{X}Phhax%Etuq-vO`oSvE_0T*AZ8tF1#a8J0te}hvKHUJd z_q55Aojn1adQrfj!M&Sdz>5N6M*q}3^1dO?E8u#NOh?XyXEhOrg&!6VUFV4}Arcn6?mLDrs{O!2B(9hd{_|e0Lc^MB__V zAly8G=X!)Fc)*1NM|K*UV|%cXq=6^9INNCNmGOHm_am7dge&aYh_m#XSj%_nVEd1@ z8U!U=1_adTSNRO(0JkS4Pte8M%i3LG%+*FWAaT@n4 zq{arU+5pN&mDSBJZ3+Y8&T7jM<@@Z6j6vT$k)E`Wn%`8_)uU0yU%4e|x=G|(`QUMl z6&sTBm|?P1S$z+wHD_|P>*{PFaC}Gr+I zqN5&WC4fLs1h=3m7G9|?D4TRHPz>&`1usIqf*ex4S^c+2vZ}5=u7#>4xkplX@M$zp z+vrH(H((Z3z(aBzUemmzRC$#WE zFjaQ;vn`^&3wWjg*I8Y;XFfEo4NhqjW?^#sK0H;O+5eq8KXa-nNQjfO?exZLTjsyM z>UM%-!M= zDdkPwaeTUYch4P4grH1DnO>J65wt8I4aw5>@MIZ^*mT|{sR1W!#4Lk=jnth@hS212 zC`pQkXyE$d?Ps`Fm4I>y#W|04r(VB$b#v-ksCm!y<>oLlcA}efGC+LaU!0%=gaurMy|p~lY)60 z4w%>1mQ9SRx3IvwQ}WBdhd3XF7x*kJMoCVFR`f=5>3W!D{g8lYIqZuP%Lv@cj=bUc zSurk;{_+CcWWVy1N&&k%3wJcU@O;bN6`u}coR-C#6_n%$zdb9~{>~Q}*TuDY^I?#3 z_~Mzi-KC@<^%!cP=A{nlTC>c;-E(W1<~j+nyJ~tW(~W7|{ABemlM?o+VaY}p;{HWN zwP5&c1R#9-74d*>OnJA>OmZH*7$L-QgZS}nFCm25}-eR#*0FJI1} zx^1b{0x$i;evgE=*J4*JMq&Jydmx&MHoI1vw zapMX>maCf85~qYR7@tL)I4HZDAaRiLh|=D)dGou&JC?Y=TExo#4h>(b##6Axe1JE^ zd8`o|c?W4Mz7RS4C@^0_E*xBe$pH}a(FDE_Pkw)oLn7LKl*LrU>#DA&IOjo2(!&9z zZ-inxp>(pMxpzz#$4p?xj2RN%^Or2yg>)GIL`!*ybL-YZ!2sE1D_5QYq5sUX;1;{M z84&E5HA{%#!F_%=@aM=2_!vDhY+dgCHy-3I%n#ZA`2Jffb(5h?L7N{pb^ zlfkOqg5MMbNDo>BR;@^=GKMwYg-96K7v3X%`?<_eT0GS? z&)~%V0|y2XU%P;=RSq7!1kPmelYUSK3N=-<^my>04D%!X5lp_1#t`vJXtDab`tA$n z5YlOexaCiu4sCE~TT3QcnCF#|NOFe1E{s!8&fgtf52sf0nL<`{p+6~QRa8_2A)^Vx zSr*^443Zh}OC$j+Xr5?Tej(ImEbRCE`Cc@jH4Z5~Mv1SVokyfF!SK=!&N+yznV(&X z>|#5Q$V}#lSE413uG}wNlJO&NbysZ9yLX4lVcFCHAkZb4RTk2tiNC|^o5BYyWBFMM;?FvEJO;&p?W(3 z^wn!Yup-Hg>PG@7#1bINMB>JzmR2n`?s^atUy~IWpF8PDPGuaHyL=Klt`o^5?-Gp% z=|>5SKecg?i33NHw97=jf@uEIsNDFs(EzZe<0Jj>#cwW1yo=liFQPaMSI6LOBz%KT zgV24hRZP#{yxq~do<(6n+C0?42B6qS(K>hEQaw!F*;!JHAG|;#P&hc`84izk@ zI!TR$tXTm{hk$X8*H0Is579LB5Yi~Q%M5Sf& zW5vi=pap0-!m(3W+QG5LDQ)0|xTfoJkVKE+80Cz#u7*}dHG7qooz}$#XIBU*+Tnq4 zQ=z?(7oVHS+p4cwit1Y^Pyz6b#FC`S&##on+&I)$lpMD(yR}FV?sKz@j!pFzZ`Tqr z5uKH5^9F?!j#JH3U3V;5d92z_FjvtyuQ|I&$cK1b*0HB|kCS+Fao1CB4?Y?PV7@nk z9BM=o>8Z7ZpP9d4!TRzUkycQ!l@*TNzUWFEBjrlC_(M@X%i|7C^3Jz8fvd`jK0*eSpC=&__DX6yhq&L}>n%T*Izqu|;ZevEH2ni^+y4+` zsD2HZHi~NE(vFXds!%zUl$0E48@Mn*#h5!2d!_i)NHKS=<+-PVNT|(N6F2oC=&Hfs-#U z85Q&BtNB6;jK|iNWA;tScGz~6q~zsq)2o3WyP{*_`|O1LLQ%nkic>+=&c;~u zXSHt1>JyBsS``*qc$Xu76Wv+oWV7mJ;+ACSwZ1Vk-L71GAFSLF9XYoKgZzw|3A*a z@PB#fj=JS8baNhX_^#~g$57o+v+C_w=Iq=rJ7@1TO}{HHeSW9?ub#Gl+)?Ilvjs}W zAH<{U{6+Wbs8^e84wu&JT6LxW-qD+Id3Uw2d_(sJ#)@-6R;7(0w-{9<4@909{-!*a zA@G;4zx%JF(fO_7h^_}&GQrC9U8u4V4sdMtk}Qvq3lfg}AdXrU+O04_!cpjIJgA(I zZ`$?6$L@pl;)y8h=>45+Y}E$?7}F6Bs5nXcYy-|3E6F8AmIhJ}EA z_Acuvhq6kPd86@Nf(hgr=%mp=NZFKTCpuMg`>fv3g<}3yippf_KaXYqKOfh zz?PhatA6|KQR8^)?oOy;u*(jT^%U6BC6*aK=eD+5_#Js>$a_n`AI_e4%uT)C`A~9* z)kb;C?=*b&(7}WL6`MFX4g%c!{#g~9)pxodnO@p!TQAgstEfON4Z_`Uq_2hg^)l>w zr z4cvoP`4Cv9qe^X2>?s%+L_(k#0L@aKrbFL*)ozX1>FmU*(F7v0x3*Yv2@9*AKYt#L zN5p4A2~sf2zX*vGU5PXv0}k1{Rb6psGpTkvI_8;9o$_;x?^9 zra}Gw9_a^Lv$*H|pOb-9_Qj)MCc1y-tXbguw~#d))Br;11E5`qMvy{jVi3aP5pQUD z<+-k|Ql&%_65E{Cjj+mtx*RB*g4xY2f9^aR!8KTICW!S2K5JLYJi}{%hXsP zd$bYBqauQerHpoKvwBb|#%Oc|sKfs&f~+yhD$ZlZ6CL)(iWb~dQTynE`tXQ1&g-wK zNci%tUj6oafExT!aTnEACT`JL7jqbeOACm3G8g$takD=TElc3N1xbh6V!@&kX$F2{ z%r^nOQlLSU0e}I$UH-_lZE*IIo%^Xjo7Z7emQX;^k)gHp^+FvNSnk`m4?s~N?&v(1 z`0eeM=}v#NzkSg`&8pN1@F&%hvrZMQ6DSCQdO&N2Rc_j9C%7i(xtfB)V{F-xTE}c{ za26<5G3bERRh(MOp1&!yy%wAy7mGH~5H?XqB8bUUU0m$PK~x=)=`&}}y!+q*cWSMc zv5ka;1RAJdy$%`_Zezmov?CsP%Ze*EUz_fnvwEs^J0>1z8m(R2>(=ds21A}Q2zhAT zrRC-3dwE2znT5!6}tvLtp#R4!@)5+b4Gs=Hh@SExW#g zhY%@E$zcwmuG3AM!d1qk41Giv=H1|97<{cy- zHlGABn{U}&V*!sb0_HG|U^3eyOs*nBFR8P@`#!Q!PY}WJJD&CH_mhMhS4j```Ky)G z93l5t!9aS5mw<0gD?_bMJwXUU!w+vY7xU*Iona|Gx<=4?Z1^ph>pAf~XE z2`%~bQnz)g!Pr}4$S9~$Vk$>5OAPhOgB^NDw*EEiAbPC2V|@yk|A`JcS8qi2=~rQ{ z6RW7B^;XqZ9cMPDipGr$DXBl-{Fvl@VLrTQhP(!EOR>V|MS<^>*P)#noe-11rWkO) zJ~2JT;O|KE;;-RBh2MV-3i!W-X#c;k%#B-p?5ln5K!MQAN4rM4o-Jbd=EiAB%l7xD z|20n1l}Og0m>95XGI4mB3HbB#Z+arv9$b5 zv7)<7Cl1H0e|g{g!sn7pnL}1-+wa4Wm#oh=0kRQfF~%&Jh$Y;{%!q*2D5L3ejCScv zML&A9&ef0d$Ma2{{O>5%u{(W#F(;wRGMyi@422`3PeRftB`GpY+AkU@1KZtiCq-Yz zBvhMG#3Egjgs}@3FUFKNbXapE8r_QNLz+G{-FiJSSf%Ij!)|(prb#67B!B|xTGTuu zX}7rY+A>bk*6A|rxlG*{@}`Sy6_^kyfgI5HO!nsjX7qft^d{10R^N?l^Qz6XOTZg! zLks-kKNfE4DRk2lZ}(CgiUB31s3kJq0NQqM*>4WGe$;ra=3z^Xr}Z3oV5Mk`t#JnEX=EaE18} zDEbwwtp^;)7KKVXX@Ps|?mWsS7~4aB5p&^-AVR3E9ogg#2Yl{_;EV9KcI3VYi-W@+ zHZZ1>J_xpU;;g`z#|(g|c)x%K1VBTCppo#* z7fpg>2Y~v67I(~v`@Xdc?qnn~0^5KA$XXfV=eR)=Kg0r-vH%nPRJ`KeajXni>rqfK*1Eolcgo+#H z=EeY|v12QJtQ%vo?Y{U7z%`!-52EoWm;zyd{D2`p=+H!;7!Y+VG@J>oP4sgAs*TH< zIfx%I`ji+3c${?V%>p!1kRpyB!5A5oQ9tai(QD6Y`zhUqjo}(8O1d5p+vA{M9!I}C z3?jFYI->{kr{AgEiCYUDWHNx7bK9o zH9TJ33_3i$7hGyo1lUqKZ}}YBJijhp0jy_@p`Bo zKf;UOl*&p!U0tY3cMU0CPNYV0CY<=EZ(`R7fS z1msr+hUBiFOD|$LvQS4a2oQ#|!GFDVcv0Xx7F`;pX8=3Ks;qc$02@5_klC+%pWdWJ zXF1natqdjd^rA>sMj~o&Hy|+^HIxFF%Ly$iQ5haamaR*L1A~$T-sIPSh|WjcQ}!@e z*bpj2oZpdmS7R4g1Y-RhG426VW$lFmP+d^x+jCxOg2#v~nwWX^>{*{0kpNj356aO5 zuZu?mwn~CvwVahT8X*=?n+r7n6irl+JQH8V~qb>T9!aHOJj); z2T)!}^tTnO0#Q2`^rkf}p(ZC9#!Ai^YDq-Ns0y$-ahzIlF$npEY-dSZkZU|#5cwU} zoiNXGuliba72`PxL6f)$feH$X5Bv0Bpc9-afi5R7ga$$151cf0I)d&%N#yF+(hp&o zn1SHn;89>mb;HcAgj{V@(WFBY0?<8rv1WQU$l@DY1K(jvg$~U%MKJQ+rV|g+fn09n zJY!t4rv2PnB)sX2zWdQ(_#nU*flokJiKZbt^*X}|`->C5d;h*#QwG4q06O9g5T-F% zSvjm50311aqtp`jt&`JnV*H(L)JaL0^b{aXn!t5N9FaXP4 z7yyinjIu>p3Lv6Tj|SykSftvV=1fNgDz*QUKx0Z$@BXTc#bP3cDhj^P$0!0RC z*7k7}MV>oU6h!)f00K^fb-di6R^7K{;pr79CA0Ui$z4X%(zhK_5kYmR`iEWE+1TP~ z$RGN0K^@$o84U3AHueFMb|5She~;6??Q~Ui*rP{#@f|8@`v4&+G>}3dlq-o~#K53m zjzLHZ762;TG1A-r-Rkl_K;DIJ;w}RH|AyZv;@m0@7(s)9j9*-svm=itf71Yg73hzT zn4o!NY4+7dPnLBI*fm7=kDi1j3yvNp&?9hg9Y^u%9ZKvgEU5|*X<_1lpVc_?bP&?1 zG3G{6Ln8;xiH1vu^EI)GY3ouJT2`!JW{w0%oHKj&W}J2Ggb$wtynDyc?19cAAJ|s) zC(HqoaOlb?9sr4;x|cG~`f&yNXxFc~K&v98H@%Zm!1br97S<4|tFC4#IUPZI5R7bVM@nj5we*cE!u-S2{Id^h!+r-yZ zxnP0a(a;fw(_HU5H9?T0fd=5XNx#~-q8YIO$0h9c?cY!q$dFAMyR zDmyUWfhbn6-s*9HR|ml~j5gnqYaVo@G3@w`l}7B|)aZyYEi{N=dJGgs$dCA=yD&^= z{Oc+VayoNrdMc|=MyPh&9$joZc<=jAyf8rn8@Fk~ChFJwb4XBFhqu!&b;9;9T`CH^{kK-oXLQv) z_$p|e;UNhL?mc_5eE*s-e3|Ten42~%;h!b{H=+U938VM+Q-$#-$3P9O$(k!p$SFP) z?d|=Yq0Xoc>&2g+W&-oPo_zf84PFHW(#TlX7-Zd zTodEk#Vk{K&ugqQSkNi)}M7}tAK5;Az z(C`bFkV{ZtU_2ob5^o1#66`?NUB9jmeeG|L|B0%IX4i50aoGjt`YuFj8ogka*}aSW zu%y|bM&pNQePzQKXXh8#3hwZ~i{^Av4?NC;iK-h;3`S}9lg-lGUP_2k@ zr}?_zo1q=_9Y~&DmzOK$nlW1>0c96T(8J(6s?bGT0a}#ikCP~Z%9+|RAnY4KK5AiH zjy~Ierx3wkn1o44^!FZMK;eoMII(DqA+j)36twdg)osJ4yWMyQd5VcF-87FGpW9-Y zPNOFPZ;+i(x3>Fgm~riH*@w$YfA<6c1O6TlpevQMdg@96Eb|T}0|O1n8G6GULeo8{ zC`TT{=-OyE1`TlznwulfWU`_e-EIT0{P1kwPP(B2%}Y*9#4La?xCaWlZFz7SY1A8R zZya0zy$HU39Asn_hMD_vt#+NL1@Bcc(bN=U? zXPxIf&$HIq>t6TToB9pk@8|pZT-STLe$Tli8W0=|5^_3i+HAnCXxlPY3eEVPOZ^36 zre+Pw`t6I0r@B;?yug|@7dSHDlLX=8sul{PvA;GW3Y7m{EBDa!`b|1ET%MCg}!X?Bt^6#|r)9-Z8>vBbq zxEetfEOhuZIPPanrM%haD{ppdhDdSb8khNxT5Hg3FxNh zUL*yPLynDmULn5(_iaJ;W=wI4q>EXnM46qqF}&06720d%h1TR>IqhbQLqV8#5~jiQ zMXDvbA2&C*1B@jotW!|@P!|ogx17~tv3b6VRu_wP|h&(Q@D zf8d-=H2yZ&2M9vT_>ftBw6f?oL__0+$vAphHyV;74wn*uo+r&V@XAdA=a6*Z{5%hy@UduJhx0+k zsg9-Pv$4P}bg8x#WLH{Mda)AH0wqm zTbm0r6GZV_jXX62wzmy%1Xalj+Rh3xmjjUg+(*jr`DL`oGeF9#7pSh8(fEs99R*d! zx)Eu3HU66ml9V8c2nJ~5l!r>YOv=!+Y0EP-GQwTZwf-lI+T7Rlc8eV#nrYaSq)*cVq zHSX9JRu9`(raSOxH zc7#xua!)G?LJOK&fVBwdhR#|fR2(Vo90xg^%vd?bXutJOPjBOggAKEa6NaR{+vt2! z&QkC=0hl2<7jI2c*3vTOfmG@f|&1d>TUFiyCl|I;Dc@>_t?L?RhkZQ)(Y9i#&Jkj;Esf4Z?0PpmeYX zO$dLEzrhD+jqoC(3VQYHS9pF%j?2$DoMM|X985Vj>@nrrP|4~t>%2LrQ}Ce56<8eL zVKX^&%c+f*B6kRjOMIn-{14ov3fC%S13-Eaa z(&*rb9A~29?^I!_)SwR0Dtt+B#~`tC>w{t+^m=tPh63sV$N2;B?_>%42vsHY=kwL# zsp1ha@l?m@I2ZxUyNc#`5zs@g*_s!pe!F$sw%+3S2>@v{>s&0qa>zE0_esl_fV^#Hy>um1fXJUBKy{E0IJo(0s~6k5p#NYYaZ5<*jok>5YpA_2=3 zkjt<}3?9kc?S=15Ho$aQm!!|`NP-lc+!Q*XgBkVLFc#BQ3Eh+rCN=(wL=v%S*c7DQ zHFl{k5vN0ZP7eB^c%KqUgW9rnE0I&nvCd3An+l_M^jA|*Eez-%Iye0N`5~lKXkbdY{pRZtjKi}wq|Qz^ zDM?h$I5W8&UO4%z#3)6WKfY0~VZ+K*BNK1jc);k)psQh|WRYS)#!g}EtCi0q0wqK% zKP_;7(dzzpsS)=RIDl~Tn9}WI@#XQzn8fL0_VC2P>>qjW-W?_Li{~(!Gfq}cbjM7| z;jz=ANS4qd=>B-0TkJ{UOK!8^3TY>|H2_o^IP3JCM<+?My^yN&Qg%5vSV3wywu z^EHJ9TE0V9FN~f(=b*2vt3t836Z3z76%^;`{>=Pk|9Q|a?CQErE_HrBprfNl;*t~275U+Zbn))QcWlXj;|*{&@;U44{CrraW~ni@mirGL zG_=?geD7k#tN0VqnU0JxNT#cHa`DI^Q;j{Ta@FhLm@rQ90Pcxc#E|X{TAi(hPOM=5 z!s*i&%QFiIAS(&wPOsj*lP+KFm6rtCDxaPHeIN~L(1mymJ85<%RhR+~jOb!EdW?-t zL^)S9|3w}Kb<&TP<$V1>auqPn7pw5|=O1WQeDLRXD)OnvdnztrThE*C%uEPtw&2Pe z-vF_LZ2G$x&qismk@nkhS_vwbDI9@Lb@~q*_RN?y_nr)K4QMw)gC3xazAYhn69-a% zygSjWU7I#X@j3pedxHd&d8+3-Yul&2MA#GG0zIAIy1Y>wq--c(KV?Xc6Pg82^|QX0 zAkcFN3abnaRB9OQy)Zo8uw90~`e5C%ez5@$?X)vy=|BAWyf*m;&FMW^o|67}Pgj;F z5^(LjwIgVy%1JVR0M@N9i)L6SG5Wl7BrKXC-@oI1;xm>x_aR1%%(`1; zJMJCrc)2+1!N-@;seB4Hw<$n5e>t`}&yhIggjOauRqS$PU=+Psz}&>C7$>8!~jQU6w<9Gvx~AXeW~{SqI8)Xp++aeEw=_p z^a013*LWQ}R-4aB%@>Wbq|c0AyiOG**CyOGvUf@f9x$AW%3yh{dDK~hJ`_om5ueMK zc&dxQRUTE`cye#ev_rnw$GyLtP#scq#gVVfk+x^go+zA`JO_G8ORBxLr=_L+VM$G<&UPLq+wrIMhhtT_POD|}f&diy78mpm7%`q43(4-0QlKHrafnol~}&#H$%f7DE9;2M*t#i?_={L}`R zQa-lr({JpQVh4agl@YXrwjGR$CDlSY2QVrLQ{kvfS;77wgK>j6D5%|M%H*pt_9Plf zRJd+s=EIbEIj`23%cM%=Tr5fXdSwRp*v?T%pm z4lR#3PKLmy#jb{YNCSk|d&1soXlQ7gq9YJ6@9?mcqg3X@O-W6|N;1pH=&4@^54dfX zG_wBDNW#i$9B2&q%K%_Yb5K3e8nHT2Y`n@c1J;3QnZ;WPJ7_r8X{@al~_1P|>`fu!;B)P_>x;3AoZN{n=b{gR#0Kq)*ZXcD%AGw$^77Kp`pNXX#t(9|J$ALPFV<&?bih;e z0p9-!*rSYe;*&gexEiofiL{N`^x2%O@FVX!Zww3xF~l;87F68%va#sA@LgT8?riE@ zv%RMI4}6NL6pgqQEK;2b2dhrf1?Oe0QXC(baWd>$0TK6LoQY~_q(LIqNO$;1r70Af z`x3gNi5!3+?Gp60Owk=h`kHJ9KN^(0wA_adQR?26&MO~8=5+YSJvuWeBPCOJo<>?` z&*smx6Wsry1xTqm|8rW7_B8f10TS51YqK&?LC@l&GvO$-L0Ln;SAYr%TeocaFuu&7U6}6!e#{@)80-xJe+SiUlryou8MgeW|m{TAtwtP~Kk{6?rA=vvtin z%ka#U5%ZQRy4xL(E($HO=_;IVIw_;k9e$kI<9Y`M@qc#>!ub^Osm;@!TNz~g#KA*#zBz|!=3Swdh zNdCgjZFe>M{r9grsy4DmvRdM-xE&~;*s$s7I$RwV$?i|Y@j~bF}b7W$wd=p%Fva3!f15M|_b^d-sz(YqjGraZZUYg@ary z@De5}*~Pv|=J!gLTz(l6c~XwKK7da+Z8ppPLFnw(z!r{u5>GYEJr~bVKbl5k=Lq{S-$KzIz{Th3avYK z?58K27aN4b^$9iwGg;$TuU@skJln{qfW7L_5=CMItR9&hG%F#c2w()R(^aSX`%IL~ z=fKaf{KS z&S*MSM3f~wEW0z)B-Bf)COKaPs zLQmzEb45;+*(&Kc{y0{2QMV}b_P4z1XPq+Q4<75>b3~3=9$)OYZ}v&>uufIcC#f#T zRhW`G3!fwU`Q_dL!r7onsaRQ@^-ya_(*9-&DT|MFY_tc$w>F}HeO?CkohaTpnBqkw zxkMcFKAYMXNuFSTPmU5kRlT$cd@sk{${{|ovHJMTU)>a|>{IfAf_5Y&00#8hBWC-ev;AhTp@>(`Uvk5R2q2FYThxSUG0IyB2>f0@1wfK57?3^F5@ z+0VrA9ZEJ$Wp#FZnNgl%Xbt-(%zoUT=F&;&jOC9B3d` z&n1cy&lE|5H6o$8+)s|crd`HaHHys&m9mt>n&2qOpo^y~J ztTl=VksqL;kGC(=<`!XjTdwFXwo~i?I4R@=VV{z&1DLZY+)7`6mn=KqQ7MbcR+^L- zJFhH&zu6Sy+p*wdqYw*k+Jmp{k8u8gcJFkp7tJ9n4<)5g(;68zXm@iXfW7e4+%y4l zR_jVj;=GCksL*yLv2{i!68xs1-eio7K7Mj$@S+$%T(Vx*FXTd@FTod^!ig@h}O zIX869`q%}MQj)bwM2Hc@EQ4(~C9TDw6Jk-xwc1y!F~!6xW0&GF$_>4ulwXw>G~CIq z!MzG#B0cGnYU)>hT=dn)-pMhex$~FbB+vjkp}>ej)dTP?`o3}HHVhK7`N)SxKjWAZ zv;nAt&VuiU{ik=XfAA0=eK{K-uR$kBT^Ueb^=6!vRS4O%hHPC3PNiJTeyA=E7Y;K( z+XLclu2|{o;}d+a-|Py<--^NuXj)Y-ryZL(k=KZ@}+ZM{SuQANpT7LOO z32yQEGKq<*py12M}t%=05OJh=LS+eRh&|4sFf4FLY4XyK8NRrap7S# zq~2$=UwbYv@G;9xx=8zslNFy&x1(r|4+gJc|2*{KQ>n8OC2-2z5h+U2yJT(5JI;e4 z<{m(%$C~$h0`#sRw?yot)abUz#2bFUcrTo>R;Q^??rP}A5xwF)+}f0sCaPAhlsPpE zMY3!QV1RKi_G504bS4e`qPUVQwJ7R$2^Zyb`JS1e`#BSH&;l^^nAbb{$w)~xLpiNEg7bx*LaWg;Q6F&*%ErEW;_3cb>GCry z<;_JdnkdY_bKxfKBVzPZmeR;UUs7+pR_O4GYyTdx@03I}{`WvIOLn*Sba%;xp|L48 zn#Tir4C>bH#)CU|&ZVwL4jMond(Wf$N8TOQ27(nvi1DG*NAY&4ZgF>_2j8LHs-7^SY}jnLA1Ic4h9As$WIFD0%Tn}WAx zs5)cTty3;~L7AR)!lN#HkT*p_0N6Ty!=wB5y{?WgVr1m8;-}9Xo@(e9^F+t;nC1&e?fdnHO6KF_^(m8U00y7ud@kQk0 zDdIX^{!eI@zG1;&bA`iO_G}^NzK7E=X1)9iJF>6n3}j!D8+(k8xdqmh5{-Y=_@4z zOFpt)Dryye}FwutAG2r)%p+K7{msL41B` z_F47aQ#Imz`)TG}R4qNBT_`D^&_-w<+k+?6Ot+bq2_uNk9hPj>M-8!9DAua=@ zR4fc?3BmmRcrYLJJlTFFr6blSwwAcG-1^7rC_KICe^6n~_7OA$l2sbaq6I8SFN-gW z8+mw(>7HL>M8$2yA2+obuHb5V``J(hV>fBKe&2f-@w>#~$;HfedvrcG} z?WrUnMsJQ+@Ai=(e~29ldl*&9YXiJ?HO;rv86NG`0O(XtVPlKlgVI&(goWcj!a^lalUmS1&zKm71; z&3F~;{&eZB&W}~H^Ue+35$NA0ePrj5@ZC?Rk8!e92&w;%w@4n+&CEMMrl-*TEwY(U zuO{9q7;H=Tdv*PzqGXP8s_fEi=b(e<=wK?YH+S{}b2VHatNo~_7=#<`@X`41`zr zmqpPibex-LX(IsFfi;7B##kD{C`N$nXMS2Q!$BpH?yA=eDWWPqt#%29My<_Ir+oSj zE2VL*jTm#xKOOmax;+5D1x~4;z`z+oBUrzGpy|7ngpG<@1Dpk{4`go=Ujp>+Z@*^y z_Pb!SrxGikba3#)lNvVbw8z4GlWO2@I5bX+1p>PDo{BCgZFnW3g?t8X!bs2~U#1yt z?^+wiG-rEHx0h5(T+-ekkmhJmB#a4b;yENZI1grs=9z5vK)8MhW18wWdD^tYsrj@} zPw+z(e3IK(mgABDf`AbTOHanz7;9^5Th0P$su94He757L3q~}}3Hb@VwGKf$;^v612BFT@(7;&1BCFfv3C@e)d zM`?#m`#c!CxFonC`y};FMn3G5_>~XT+d5uxK#f#H>xcVC5d!_i4&C3%rX0`%x@@%8DmzLONpP9yI5#Z(WUO9N#v7%AUAKZ}yob`edyN zD~pr|$u({CC{?^>bk5)JyXEFJ-VDjy(XD3CFp((Alu}InI#plBtlmR}^>lbwRtMtA zqRZp%({c=51Nsyk6&023q}|0UC@6?R3~|YRUEkU}_{22&=A&mVgQa>#0S${GI3YM+ zPd(Y3pv1_~5qCaUF(IAiNhMK@*$K`mZ;s^{QpthK>6wjh)4q0y`x>1?jo0nsCWvbl zs|>M7E5NgKKsL?WxqyM0zWyY78cN2X{?*WCHd+_9dVtbBM5#&xbMJf#-@YaGI~q zuVXsTc5eM>zKJC?5}!rVm;@riq{H68aK-eZIubxaZGD*ZWRUX01qMqf*QJbQ^mWsr zFLN_$^9GFVRfCny!ZS5UMg{k@U^f!WCRK`9Wnrft7gd80IUJsJhX$R7A|*2q81E@N zBltOJqD;Ffk837h@bICwMU~S^}1?i|wp@(wd;FNe*nVmqcS=(T7Cb^bG-tj*;EOFIk z(V?dDMcREZkXte5+46GpUv*&n?(1}9W+{E8PKJ|ZqeL|^NnwJ~{gt#-Z@4dqp~Tvy zKYcnRC~k?O_XxXCA~inc{hig6Hj3k5cL1DwCxeJ09BQ z(QBtJnQ`j5wh`_f3WDF{yCu!Ly1Tb#%_hzM1`TVbz4)Med4uoPTV>NH^b0x}uc_b{ zY4R)X<9`>MsVlvVyaNJ72#LU?UDi`>F0D}pfQt9YOHaF!J|e#4xg%oTK0F_AbHu5)95o2inv|Pwrm-)8l%LSgUzl za(sAhIz}X9t-@DtJZN?sMT8A9#_&5T9{m7|1~KB5QD~diZB!`b0g=y}U=`C_Wkt?o z&yqDRahT&@$q#9C*nd1iAyq^2BB+Nf9-kV=7B%(kphwH)Y`f5Ba#;5=_%so1^90GF zj#>ZduSB)F3yeC`zgD1@3xFC7qFzBNX-{EykY^J)Y5ka5b4%N0+gnD{=fQwZ6`g?S z=wU*Ms-8~sEb%-V3~`iHV^Af@`#=8h1eX5$`8K3T9v?c4bzZ==fG(X0Dn}z)%^Z49*%1pC@v7z%Xi4I+0Ya1!iCDb2)SQZ5TByXra_53U- zsK{y(bqUo*(+VN=9=<`?LY+!@-rHkVVEHlc4$bEj|A&|d`IfF^K~pwieB#iueJ)T6 zM~9Tka1g-CF4N?SDg2nR^T9j>Taru0$h7xvl^xt)HC+}DPCNmzMqU&isY#lH*3Dk9 z;8S%O+n&S-hVy6}YP6MnMgY=3|R|0u*z3g1H=5u1_Bt z>_0xyOsDwik;vCVX7Ox@(1N5S*tNqwM9Yu-<}h0RdGk8lmxfBgyrh4-w;jYcH=;u#Z5*LH4?T7=Njc~ zW1H=N|FGd6OL8f&#R2)qj2)3LhyfP-hSRfS$(`Cbrih69f89Z5pnr7WIG#r~p2OnO za6HW_+FA03hg`cJBvgr`rS!X;1!L#qTAKx(0| zf?@xh;Vi%H7acKi4=!4nhhja$O8Oe1(KIyD!XoL3`m)%3oht$<1SKtuaoq`}vYRL* z=U=jq_$E@eU_>)450s5W;!g0JELYIAfn`+7;Z)d^?Q8)<%jXb$^B7uO^-2zdGbDAa z@n<5uFE{8M7R^Y4INS$f`K)VH8}(Ee(|^KSd6#SV{sEJy49wh~{`Yx0P(OUeC+c$G zQJl$DKt24^;;o}_8Dxqg3VCs^6w(LS&l`84@2^CHyJ{Ure6;yptY|{{{o_z&GfTI+ zob^8MUftEdumRUz6(6?dZ&K~5{G)v9&b86my%o-HZaG{D8W_`Ls}BQfKUr^v`N+Tg zD`4lyFBe*W_m;B${$EFSCipdWjcnquY>D4w^Aaf&c9tfU)w>XXs3y9O!Udgw@7Uje z-0!aD0&Txrm0Fh^OoBuF2XBbUz1X&>c>8V_zh(vIk%QA_C;h2=JngluR`ooion^l( zT=}l{D=y-HV_z0g6Z+|=rlExj`}pzW&Q+Gsg<{u2rkgx{?qa#E;1snwDI6@cN+R(S zhbL0BG#;ywzaj;?c@MrLk1OPA+=nRCnA|%n_n^9xpddNWAqaqYnT3NOC8cNY-a@Xj zYe@nua^8|hW10t2X!Bez_M6;*yZh)~B%o*}g0|FmP|uo4q|U zS04Iti1$=zqh}1^MUSWUrPVwd&nx_;sR_`7-xjW#LQ#Nm*8GRLTfC(=4z_d@h2jr z!r2KG2RUVXu~6C%G8Jr4<;hU_2!$*5{xcY(kV9F=%S6$g#5|2x| zD2*6~c+qY?x_49h>qGC*+(S4_rgnsoTwOZuK2Sj|0e}!; zuZ6c{g2}`gYU{z43G(8qoez0`9IIJ;^zv;j=Vz#A(1A$liEaV`adZz064r!>_?4;y zI`*+>U!0f#ISTucu(VIkVJ>LP*Yaw{t?$>5zRlw&2@?DY3Y$plys2PNv=-BN_zZyY zIA**N^xnu0D>Z9CSk8<4bu<8Oy&1}=p=-lbkV}(?N zAELkYv08?H-&O`DGW7Qxq^vfEf{H|?n(0Mm)$)0A9YG;v2A}}K6e_e9lDCOv#-9sH zIOUN0o_l>=YNDO1-Y6BaI^p7^KxO(}S1B&aq#4frwq|a9IqP`}B~(eqHZeYfxi%+3 zR75o7T*OmQD-XxTE6{_e_|Ydh7s>8(?cJGirXeD&M9m?SO&VMlgB2*n$FjY_{|EF! zZ)dv!l1kn~PudTT3DL%+$0jUMEI@{Ax5Qe$;+Hdk(A+89^+$KE7=%?(HXLLt&LKVd zuzU*9O#zI`)G?$9<&n+xq=JLEHJm@cpPr;(@{q019)g03$NeI=XO5zDWh3KMgD3QR z*Z~OwIC>wD3}{hgh(F@IWG)Cck*}3&Xk)o+#PJL23&!y^NwpSAN=sy`|EzSs4|~h{-kYcxBbWG=%dR)=+HG)qhoruT9a`T zCZLhO@$g`6F)}LUEW|8na)~3}yuC$L_D|?AUSXnp=CdVdCqDy>FG2c7qoh#_JYcvK z4UO*Xwi^<=%vJo*YF?bz!Xw$Is=sR2IUjb$WtD=$d9VN9RY1E(wo|ZdUwY@p$ioVL z+y6#L8aX@H`QmZE#H`OerT<9nb3%2)`CFBar`{;lT{yv#nHuN()opWsYJE-BgWO3C zTRtOcDzh-;LZ3}k(k*Gw-^hox6$rh1nN$3|mwE`iE zB!N0uocm36CdkTEU8K_LtrShd7Z7Bp@_0coVDw3Z?*=gDGbt^;Biyotz0Lk4A2H0i zq+c~!tJ%Nue7^fDRrhhNC3`N^?KQ$;ytKQzKg*|q`f;?0ddoIyM5kuj-ye;U-0Gm< z;7C{`0EGtU=E(;zT9R-3mSDbqGJAuQpKWg@km~&O(_PtoW6+UHZHRU5o|zTgLjfkv zW}Sxlkw`#+#F<>r@9&rS&mtVZkK?g^XQN355^UHd`^t0Z45(!XupjcW5Fvx3KQjnh z+Ak3=BfEO&1j<{RDeHa`VI`nquQEx8%6($GUAb%l;FI8~5=}|)7ET(vcluQG&JNUCNofEwRZQ=+_`XTnq_7dc*;6m(@3QD)*sWh0GvNq0KhTi&_DFoP`LtZD zd9G*NjOiB_ZhY5<7Z+9y^~WseaIywu{sz@SFl$i=f#u83^pFX--N$?G>EQ&LB{?e( zO~M!uhJS{D5mYVvlb<@C4Pb{Cva++?%ah^;5xvC`brYMU&UPB}edTRcJF`f@ePx>@ zL9?+lr>q4D;`9RtSEW9NpQlMh$;DF2uIkDE{oB!JdQo)p0E|Q5P znFcJsd-o=hMeT4|+##40qzr=>AtwBIloZUw@*tZ9E=GlrpY!LJpM(CXu?SHz0GG4| zQohi4CqEg$`@2q_ywg$nJ|Vi!hXrF7Zl&DT5D{K>iiN zfK+gToy0_J8Z5^JF!sbfjY0Al15tg6Zuf>?YX0{I*M2%`U}OJ^ivNVfZ!zi4ezWno zaa(aLQk;ts&2&*jRcwjOdIV*p5Kd))>THPwWB&pXkM)j}en#P`Y2e+drw93Nw0tmc zt?1tofBnrHS_`*aczkYc#A%Pl4gMq5 zQ(c4k;ESkW(Ur?aTIA^?A13$yax$|;XeC<+>o$_iY*8=CcvWgYLaG^4Q}y&PO9t)C zV5bV3>bJdhXXxqO-B_kQ;t$xG4ufAywq5SGwf`;+wBnyDn~R*0_K7b5v<=kt`D-t` z+Ku?ozk~67w_F5}A<^2=#-MbN;@{6&AJX2F(tn-KKa=Dx4Obm6Y|yoedCQ*OL$@SR zLR{O`YkXkKEC^t~^x~6q$Qjh#JcF`sWiSV=1Cm$_E-%BXCmQ_Hga)GWCkQt&_E~vm zJhiF@JA&->`%d%RNy@i#n6y;a&itnD2g98j< zR&)i&mKY|YH08C7HNCCIrXa_n3xg)o*D<1ew&7bf{ydb70zmdpZk2FIA)}It5A$e= zh)`15{*ky<0b_)*1Py)At+SM;>m`qN=KHZu7&152r`yc-|=&beth4 zBY(w+>!|-5lu+Wb-tu`Ct9$+$8FuS`q)ONRo9VFsuZhGioCa=|GAqW@RTQE;4FZgb zdcEXxUZF9d#T1F6puRYZuWG1W;FSkrBonPGjm`I;k#_HA%rMSW`Wask(9YZR&%KRK zb?bF$o2#YgQjNX29c;%>*fqgledpTAQ(g~H?7Uz7=*@Ezb{*9t9ZYY^sVhr64PMo< zZ27F|-F^JFbyHRzk)`3};<7BJnbpuo)3f}JmJL7qa7IB~P~}3WBBvswxFCRfD+}u` z^HzSf=u+85hj!Bw@M;M{3barwEUY`ZYgawcBqHZ~e! zcX)+8wI4Xi$g#~vsGeQ2p9e=dj4Jo+d1iR>`>N*qyuEjDmUq?B=|UZ|O!|%8LiO()`8i>OgF|;!)n@i_U21A-7JF`gv0>5LVP0Kq%QedKJodAdE}?|1 zx~eg9Ohw4*{%_KD`M(*xe0fiJiY>sr{I&@@wIZQ`R}jYL|3G=LgG`Ioq<<&DW2jp^ z>EZG0MqtMicX-u4lg_ka$o;a(KG$EZ`B<+ke}V_$;oM+5!#snBz2>W?eFyLcKYnu?%lip=BBKyq0w5r^K;w#Ki)>v z+RH0ze@jV8`S3ilpO&_E$CsBMn~$}(*If1TN*({leV$ttUNbf@aDyJ?thn~@VRI<% zU0u>T7KS$;WDv1>^bXz0X96oOH{6{TJ&W_~3k_ZO-n|tV}_1Q zedx< zr$HLnVb{{Ro3D@5t@+U%P~SU!iRa$1umL3SdZIp3SxL_6>7*_Wo(CrN#07Z}?YJ=5 zrlrG2j7UYfK9P=V$YsN`sgLPbcst5a?3&s;2htm%fg7uE4#=6|Z6-6`lgyUJIKHKx zkI~{x9lAQZ$D&1xcIsR>6E?+ax#wf%|Kw9je+N+7Kc&~7_Zz#71ZWa3llgNyARn3VGHd2clNpyURw#EvcsR80!Abk>E@PU@4tuNl7nj!m-EHK; zg$r#;3Jfpm&iboOHL2+~_`@(%hK+NvDSW!ixu)D~P+aI{JMc709Qj_R z4o5fFUmuvh0&mj*VuX(%7qc=wY!G;wUTDUWC0$##ls|<;d$2p~vH1s3(2GG1-A++H z(&#rOC4SC#JLnIYHAKt5PxI4*`}h6nW%e2c4=xOYjv!35p#;SBXjFIOh7B?dXIyna z#mbp$4(L98I(BBHFC)QUrS979q(3gG2G7(v0L#pz zcO_a*M*1lJ==axO<`mOC4&VP>0jiwgku7lI?lB7Pd0UbDN9!y=!=TtHo83pB^aEp* z-#$8JJfyA&CWyD|F{%aMg*^v;ajCTrh45rIs&RxGX{u09glDJRdF&Lv9~bwZ1auw_N`20nU_~o>C7o&fsdqH?aG?rLlUm>0wfROy<1s^Z zzpaSXeIFjL%(3QR!%JPx&HC%x*RNf-j_%s*0FNyKQp<}ujLML_!eLkbYE9Sh$0=~$ zb?evnQs}Y&?gxjSO2)Cx20QG1%*!P zq6Iie%u??ed7nRjew&})_j&jIch66)DlRUzfHGV<{EUGfo0B|>k5}DRB@Ow1mdd|+yEKHvBncU98V+xbLW(K@&wWmX%=&92tBZ;Cl`3xC6^B1=YVL4xywmkW)Jrx430FXE>Bh@xv^SjNs5`kUSR`jId0Ed)`6 z8sxC@KwD^ZVDG{}wK9J47YMe!hYz>owUk_{F5W7o?c4F&CN2Y`(svHz&(!%Vv<|2E z*qRM>=T`)+pRd2)-32N@)RiYZU_o6EOgb~HQReffR0N1Fbk%Q5hN6Ux3{-6Lx)kta zUYz~l=qp!Rn0Ue@>H+z@Uf;K3&XqM?dGS@NR@uCN&_1|SoTZ%<5-LBB2(M9V`dmjx zM{9fx=u2^9XUd3JI_wj6>1OZy>KrzJ%HJEZJ;b)D5PsO)U zu~k)7ACS_wW|~*8UIqv&hHYB2e!ax4U_NgIW&|+PlvId4Bv*`7xRaBU^KsRssb~G+ zFt+`8anbDPH?LpcNlKdVJ+NVK zR|BW0z+pF(wx704Q1xAP$M;M(!=I&--gY`uyvVtHD^{C8_N^g5`4?f0OiT0LzZXji z+ll0z-C3;mtmoeSK^+70rUfYt9XqDNs?T2Q1FENQD+PX?8I*${qx4DjW}Kx@u(?fw zV(7hf^eApQx~rSzzn$IqYI~=%bJaWS(r_qUxzuw8o5-zqEg~r^P7>v^?Q((srtp(~ z5O$-vyljFDy$PP&zDa}M&{OSGZp4lv4yBhyBcf*n&iYQy*POJlz`&Cz*L_)WoFP7k zI~&cKg|c@udliNu)BA;JfQ7u|DBjSs2(Y4nF z!H>;P6Lz|@dR=9y)y|qn<{Cy}b1?QjX0F@$(g{evhrQaidv}zkA2Ww17`uIYFzDeC z{nG`nN5xV}n$4hkk8c?kQ}N@+Be-pqdq)oKTcOcGatMBu5u38JT`xKqv-AL-69(|L zWo=D5^W?z*^!jYPTkDPTJ37eBTVF^2W>V75*#S!IBqg;wNsoO2PNxn{d+QzK;?Q&5 z?kCaORCWOzS!^e1`1;rCg=)7N9vHLNzUFkr+A$OLSq+T?zfM?DWnhi69QNRKgKK=Z#2 z{(--}ZN$8gaqW*zapzFK{^CVPI*n&glDC2#+M9gHiXAHclART9%8PMKe);-!l%Zi8 z%GTwn!7k}52A^;$ovU_xP5HOwbO+mBu;$n1Kj~?%(P4_ukfsBVHle#2McD3~_w11q zr%c&mteV9B(Idh2^!oWw@4Hb`&0RUOPwNd70oN7BfEOn#bz~x?Hd(Fi6lb1H99khC zj&jyby}{<|i%YB9_pNC3#~-7ivpLr*FB4pp6h@AH7H!>|E+@C3UaGjGaOaATsBP29Lg0e4Op70hF6xNf}Da%IZwO@B~8DKSr%~$Peh7_ppoeM|0LgUs(^1 zt-E%EC>|9!>qchg+_cvTWO(1aefxP<-ms|ae-57-enW8#vh-JP-)^CpxOwwtE;D9s z5P|c+%{44+wSRUq3pK?x?52EmIxBxPGPZ|kJ*AZ(RZ-R0m1?k$@SnrTaS17U^JX&? zZ;O^xb(H3-_U~e@p4)i;>r_c=UfrnwwFt2L&i(t8Yn|0l$X|($KI!MDKwq=;DB=Zw z{yjT8dqY9s*_E0nLhw|K37j=A>uHc-KegN3)R5Bsg8^W-@YpP+U;R>C9R4gcqi}6a z)y8Am4sEX$ZQk5BaBooagOj#W4~^=zzw6+^ZNTdWy>LdwPX{w5#OMuSI^pShdTie5 zbu!bGdijt;Ri~<^l>RMc)3)4q@3tZ<47P1=a%7NMGo}uB!jHsGbxI${B(e0%n*wM3 z_0{SKMU~Nh$H;k2qm7$&Yt_3Yd(;)1{8rlW`&Er2h`|=RJ zMvxIy7HY^Nv8@qa9V9qIJN|gPZev`MQJ>c6msAvT@xJoMm+c$AABD!|5#C$ls~cJ- z)Kv6iUQ&I9A1u)&YKkq|x8KegNyK6umdrJ}X!T`xTCk)#4O^oy-oGXL&X%ySFny<` z{YrX1h^)NO*KQPa)EaVQd-U!tZ6|_BrdnxoG8DY@o#jNRVj`_1vP{{MHsqlxXh1`; zd0`m(RqaPiv zq_Bi4s*XvM`1p99W!um5Pfqt^fzoncxX=QsjrLo*Zbg$XrKO=64O#grD=(aVIHdlB zN|TupKh+-1seaMXp|I1B1tS9VE{7ahXjPP-zny%EpJT?4Zy-D2gX1GIPTuvLm8H?s zS?}wc_cyX*_H=MrHt^`_ADOgQ_AgADpZ<`ok+gBo^9pQow^(tFca7gnfz|-jC6QuF z4ZX+RDC?CT(pXFgF*1RQ1maMrDSznFt#9Aop)>${hB;RaV`9Z_f{&LV0h@zxEWO`> z=1Vm?+&a@s^LR68lp&d?G742fU;g1Xy(p{I?IS;qM?cS5YtbmgXeWEFEYM-L1+8hs zl^fNadP3{>Q7u}uX;YuS71H(CumT%R+W1_0g%430G>>j8Y0? zW@GI5qtozp_2{9$eET+)XVlZL1Lz>myM6CcNg+&uy` zL2=bWNy+QRjnN;o<2pj6U3&3G>6p9JOXH^hb?}IelO`g;-AGkG<|HV=(~KQGx-RF; z^aTshimdlzuS4!@J2ggM?e#&Q`FerRCaLUGw!?!hB z5&8VR*UUk;TXgQci!!|vkHnn^54bqgDSrX(py2O0RXL&R+hpU|^-bv*LEt7(!fI)0 zo!6=Sa)BMOX%i(K`8}34o87h@J4TzE_u;v1(WT37{3?qPJPvcIxti`Y@v+C0UWtg~ zzd|~9A2FgMaZrjwS9Ky)X$u7eg9#VIZLGHp>wB@`$HU7s8p0!eKk9xcvBlmww`Waj zQSWQ$5sPkf+S$it7lfZH{gxS!+sGxtqx9z;4b#!(YK!OnVHWe@D<;D9`8623j*$JL zkbBhZ#C7%)EBo8AJ#D+Ednp;7zU?dat*_zgS|6R?6cu^#B%I>95n)HUVd$^aIai}I zEzi7TWIESt&|oQYZg~tgd{M^Gxc1$>Hnj5%kZUDDQWj=;j6Uh=<<;CZC!@+^>(;?n zTKlVFmfJ(JZeA(d=F6)a|MXP<@#Dv{i1SazZrCuas=9h@b%pJ*VQXa}1NDxkx&5(n z^w_cWa6xWmPshCe^E%0m5S)(?xkJBS)U#Tl%KC*j--^fV&bxJ20j`f=!b*>wMF!mK zghHQA8sn7k4~|~5MvaQpmGBVRfuBWMw&UY`L1FqDhsfpmK5hP}e>E-+z>(7vOxY-J zs|S+CTs-_Ao?YzDg6Vj5U*W_lI;2Irv#kBlxKC9nEU_0vZtNC5!6^U7BTr5j;E}RD zks71W$_{D7t>IB@Uu_&J{JTf|?6V}Mqd`u%HF|fIC)rSmJMQ~hZB=hTE8U}4FU?1n z4rT0`rRzLvZwHstx2OHN`@2s-L3VO-)3IBSAj--fyM-*E8_CJZD~>;ZxKeZJ*5+rO zf5c6j8g2kK$8o4gKXsjvUn+Gyd1~;zTtH(%y*Vl@;|jt_6}9a(6iu){)yVU7972hxi`4*Nd=7sb_6L;+2OmGdEnxR@oi#@xlskNZon;tkzV0T3m z40$8Lxzb~(2bm{$!TV0;eKVgO+SN0HN(=qZRAm}Xu`}P^h*sCA_ilh)?x^M7u_FU# zodOYOGrd@z!RXL)=_?%R@8P{eit&TEA#-t6XZD4rvbJ_-mfsH&Fs)*WUr)?><_4~{ z)ni72!5go%AHPp5^Kq|;42&6?b&}J#KSN|(xxdEb#iqPjW@@_Q;oJuG>S?T)dUJ2H zX>W%m|6$TZPA0C&MQ#t><2;qwbLalv&COVK==|}^JLCfE+*-DZWxdAMb{yzDGgx=? zEC*L+U)?x}(!)!(V`Oq|Eu*(LVt3)0^&7W6ib?rijO@ zuH22Rhln3phND`@F~t?8uP{L@Z8TsZamBfSuVbf9ZT5EEa7jMW2^(nt-P}BK9>?Tj zvR{n~kuxl%&oJuUP4>a1t6dJ9s&+T;>k?I;M{I51blSQQRsDoDuj*2EKYbaXH~!@G zk^Yab-MV!q#kcR0KkJR^|KiBuufq(6HeJ&E?_2g-ZrQV>)t%DAJk_b%M{+4>A0!_x zy_C?P^K&lNWj>b&4;-tjym)PLlOn6B;jce_^u#?pve*7sX!VW^x9){hH2EIS`O>ei zZ^1_~5F#!!_=L8W*3F(PL6(!~j&F_VNcz_XhHEn3uf3#zh7U1aUPUnpDY8?s6VRGEtg{e6^hk(5KCr?2SNfr>F;5L>%Db;E ze5%6z@cHDS?hY^Br}ZDwJF&UbO|AOA?pj5~y%}8cE$;4a>h7==5tUal2<{q!mvb;JbLDNM_V{tWMW?6i1*%Yh*7`R^`K zZH5jV`c>ruA0-iibC0NCSRH;o2|MQuW8(+CH#W~T{}+#@Me~HU%MwZpGn!LxDacdV zAMj%3#?{p2WN2=}NAtS^we|FiT4&o|{1p4D)9z}X|1&U@EloTXO-?^H7pE~oRB?L3 zt@N%tlH-g;Y-R-Q5qH`ku#rL?<=cwLefDwyp?1G;VGmL6pg=7Y3<77Rg_})7AsoQ+ z+~HF8?b|6)0GU>%{LOy=>nbWvoOEWNqRHD&pY(Cs)dSY4I}!|u-I#1TKfO;G`8FCn z78k3L@6DPqV~J6(5hHv!UT?jTG001GDdgW47Ebn5M^Vw!|FOCCqV!e4kxo88I0*G> zaq*ML-h7_h(dm6FEv-d=AtjEHus@b#A;8sDp;f(2R5~971>QA&%+#qCak3`=t;a z8~dk;C(ky*X)h={eJ6{ZOy1K2J{DY*^r!QOhI8Q16&mzu-PY;Q{{3TUd0SP^4^B5= zEt+GU6R5c+j@<@{+RBSXaFpQ{urDMokHlJ_%niYve2PTaoq5`1d7aAfE(jkxmi(B` zcb8q#toG~r@txP$Cy|SMrwwggd3tg4YNZkDJ^8L!oy zvLQDi4b9BO&L>WsK#k?X7SRrnd#CzZgp&GCbS7v*2uH{XB;(Q4=Nxrwp|J1%JG=u5 z!P{xQ&N2dH56$$P8XCt_>;o64z+DSp25j2DG-P@ob6|D?@_X)d%IKnjer+Vk5WsTQ@X8 zXThfTd3kvq&GJ+)cy#hN&XL{p6iv0wV;7Vj$vt%7z@wa+(xEK4s0*=x9)QM^>4R;b z&lPaIooWxj#f$M41FtX@6eg#g+^*iP`XzI_FOKRvx9j!UIL0m2W@QCPueZ*nS%CqWMc}EIXMM-B5>3gsdZmxNlXObL4`~Jx8dHN9{Y=~ zpe@ddvy!p=fa=*K7}EaPUu3z1>U!(rCypKRA3JQz^JQe^4_y`-)lR1q5{hASL06I( z68v9f_oZxUZf42}akxF$UsRi7eO?b6hRVCcP#GSreg>u)d9-`LtQ62{jg1R3fWl;I zXlyhgBTf%8bADKBBTThK;+H=D#u@!!`l2W>)Yd-Or|e#U;)PQY@hI@SBdeAhQtAF6 zu~q>yN%!`;5Ucv&AY_@}f*LJE?Jv2itY-b)&`ezL?fuH6_ezaY^vZ^_+`-g}NbpWI%u@WZ4IarE* z=mfz3M9djAEF<>!rIy6adoyZ;)ff^fhFi|E%uE__vxkq**g$}sr3X621-#LWjU}et zM^Wd=pqA8hZN#*UUnoKfODp7Bff2g0_#P-X1NlpT~bHE~cXIWmB+qSauU z<#!#6VRonChUE};vD4}LEO6+EbTmuGlQqIxJb|g{X$cH_Sa8Q$J)y%sgXLUjEOl~sV-smzBdHMOC5I0=@-uhn8jK!p!OY_FX$sW3_zwp9Mq711a zs)hy!aDN;b`7M)d{kcU@jw87pIQa6c$$`-AAg}S3*|V6Fv2{i*l>*Y-{EVWnzatN8 tt8t{{zplvt*zA~J!Gv+!I;#;XHXL6GtVv$+Wq)%-j~$5@w8UH}`UBYD6Q=+G literal 54780 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 diff --git a/docs/tutorials/cheatsheet.rst b/docs/tutorials/cheatsheet.rst index c26a325fc..f9c90f290 100644 --- a/docs/tutorials/cheatsheet.rst +++ b/docs/tutorials/cheatsheet.rst @@ -48,7 +48,7 @@ where ``env_fns`` is a list of callable env hooker. The above code can be writte env_fns = [lambda x=i: MyTestEnv(size=x) for i in [2, 3, 4, 5]] venv = SubprocVectorEnv(env_fns) -.. sidebar:: An example of sync/async VectorEnv (the same color represents the same batch forward) +.. sidebar:: An example of sync/async VectorEnv (same color means the same wait step (same batch forward)) .. Figure:: ../_static/images/async.png From 4cbdffed57d056739aea6986388739a2b157e12e Mon Sep 17 00:00:00 2001 From: n+e <463003665@qq.com> Date: Sat, 15 Aug 2020 06:25:44 +0800 Subject: [PATCH 53/74] move wait_num to worker level Thanks @magicly Co-authored-by: magicly --- tianshou/env/venvs.py | 21 ++++++++++----------- tianshou/env/worker/base.py | 1 + tianshou/env/worker/dummy.py | 1 + tianshou/env/worker/ray.py | 3 ++- tianshou/env/worker/subproc.py | 19 ++++++++++++++++--- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 867067ebf..2b527fac2 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -188,17 +188,16 @@ def step(self, self.waiting_id.append(env_id) self.ready_id = [x for x in self.ready_id if x not in id] result = [] - while len(self.waiting_conn) > 0 and len(result) < self.wait_num: - ready_conns = self.worker_class.wait( - self.waiting_conn, self.timeout) - for conn in ready_conns: - waiting_index = self.waiting_conn.index(conn) - self.waiting_conn.pop(waiting_index) - env_id = self.waiting_id.pop(waiting_index) - obs, rew, done, info = conn.get_result() - info["env_id"] = env_id - result.append((obs, rew, done, info)) - self.ready_id.append(env_id) + ready_conns = self.worker_class.wait( + self.waiting_conn, self.wait_num, self.timeout) + for conn in ready_conns: + waiting_index = self.waiting_conn.index(conn) + self.waiting_conn.pop(waiting_index) + env_id = self.waiting_id.pop(waiting_index) + obs, rew, done, info = conn.get_result() + info["env_id"] = env_id + result.append((obs, rew, done, info)) + self.ready_id.append(env_id) return list(map(np.stack, zip(*result))) def seed(self, diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 4dece8bdc..8dd153389 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -45,6 +45,7 @@ def step(self, action: np.ndarray @staticmethod def wait(workers: List['EnvWorker'], + wait_num: int, timeout: Optional[float] = None) -> List['EnvWorker']: """Given a list of workers, return those ready ones.""" raise NotImplementedError diff --git a/tianshou/env/worker/dummy.py b/tianshou/env/worker/dummy.py index 8d6e4e08c..97b7087b0 100644 --- a/tianshou/env/worker/dummy.py +++ b/tianshou/env/worker/dummy.py @@ -22,6 +22,7 @@ def reset(self) -> Any: @staticmethod def wait(workers: List['DummyEnvWorker'], + wait_num: int, timeout: Optional[float] = None) -> List['DummyEnvWorker']: # SequentialEnvWorker objects are always ready return workers diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index be972bfa2..3501e5714 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -25,11 +25,12 @@ def reset(self) -> Any: @staticmethod def wait(workers: List['RayEnvWorker'], + wait_num: int, timeout: Optional[float] = None) -> List['RayEnvWorker']: ready_envs = [] while not ready_envs: ready_envs, _ = ray.wait([x.env for x in workers], - num_returns=len(workers), timeout=timeout) + num_returns=wait_num, timeout=timeout) return [workers[ready_envs.index(env)] for env in ready_envs] def send_action(self, action: np.ndarray) -> None: diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index 0999ff22b..4c1308f06 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -1,6 +1,7 @@ import gym import ctypes import numpy as np +import time from collections import OrderedDict from multiprocessing.context import Process from multiprocessing import Array, Pipe, connection @@ -154,10 +155,22 @@ def reset(self) -> Any: @staticmethod def wait(workers: List['SubprocEnvWorker'], + wait_num: int, timeout: Optional[float] = None) -> List['SubprocEnvWorker']: - conns, ready_conns = [x.parent_remote for x in workers], [] - while not ready_conns: - ready_conns = connection.wait(conns, timeout=timeout) + conns = [x.parent_remote for x in workers] + ready_conns = [] + remain_conns = conns + t1 = time.time() + while len(remain_conns) > 0 and len(ready_conns) < wait_num: + if timeout: + remain_time = timeout - (time.time() - t1) + if remain_time <= 0: + break + else: + remain_time = timeout + remain_conns = [conn for conn in remain_conns if conn not in ready_conns] + new_ready_conns = connection.wait(remain_conns, timeout=remain_time) + ready_conns.extend(new_ready_conns) return [workers[conns.index(con)] for con in ready_conns] def send_action(self, action: np.ndarray) -> None: From 28cb3e4b2eb7bf1159adacb78c858114f8a10968 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sat, 15 Aug 2020 07:11:39 +0800 Subject: [PATCH 54/74] fix pep8 and move checking empty ready conn to venv.step --- tianshou/env/venvs.py | 7 ++++--- tianshou/env/worker/ray.py | 6 ++---- tianshou/env/worker/subproc.py | 11 ++++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 2b527fac2..c4997a216 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -187,9 +187,10 @@ def step(self, self.waiting_conn.append(self.workers[env_id]) self.waiting_id.append(env_id) self.ready_id = [x for x in self.ready_id if x not in id] - result = [] - ready_conns = self.worker_class.wait( - self.waiting_conn, self.wait_num, self.timeout) + ready_conns, result = [], [] + while not ready_conns: + ready_conns = self.worker_class.wait( + self.waiting_conn, self.wait_num, self.timeout) for conn in ready_conns: waiting_index = self.waiting_conn.index(conn) self.waiting_conn.pop(waiting_index) diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index 3501e5714..544718b98 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -27,10 +27,8 @@ def reset(self) -> Any: def wait(workers: List['RayEnvWorker'], wait_num: int, timeout: Optional[float] = None) -> List['RayEnvWorker']: - ready_envs = [] - while not ready_envs: - ready_envs, _ = ray.wait([x.env for x in workers], - num_returns=wait_num, timeout=timeout) + ready_envs, _ = ray.wait([x.env for x in workers], + num_returns=wait_num, timeout=timeout) return [workers[ready_envs.index(env)] for env in ready_envs] def send_action(self, action: np.ndarray) -> None: diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index 4c1308f06..29502d065 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -1,7 +1,7 @@ import gym +import time import ctypes import numpy as np -import time from collections import OrderedDict from multiprocessing.context import Process from multiprocessing import Array, Pipe, connection @@ -157,8 +157,7 @@ def reset(self) -> Any: def wait(workers: List['SubprocEnvWorker'], wait_num: int, timeout: Optional[float] = None) -> List['SubprocEnvWorker']: - conns = [x.parent_remote for x in workers] - ready_conns = [] + conns, ready_conns = [x.parent_remote for x in workers], [] remain_conns = conns t1 = time.time() while len(remain_conns) > 0 and len(ready_conns) < wait_num: @@ -168,8 +167,10 @@ def wait(workers: List['SubprocEnvWorker'], break else: remain_time = timeout - remain_conns = [conn for conn in remain_conns if conn not in ready_conns] - new_ready_conns = connection.wait(remain_conns, timeout=remain_time) + remain_conns = [conn for conn in remain_conns + if conn not in ready_conns] + new_ready_conns = connection.wait( + remain_conns, timeout=remain_time) ready_conns.extend(new_ready_conns) return [workers[conns.index(con)] for con in ready_conns] From 5863cd667ed6ab0f3cb82e1fd187fd9ea2fa4193 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Sat, 15 Aug 2020 09:02:38 +0800 Subject: [PATCH 55/74] add assertation for timeout --- tianshou/env/venvs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index c4997a216..3fd9b7fe7 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -76,6 +76,8 @@ def __init__(self, assert 1 <= self.wait_num <= len(env_fns), \ f'wait_num should be in [1, {len(env_fns)}], but got {wait_num}' self.timeout = timeout + assert self.timeout is None or self.timeout > 0, \ + f'timeout is {timeout}, it should be positive if provided!' self.is_async = self.wait_num != len(env_fns) or timeout is not None self.waiting_conn = [] # environments in self.ready_id is actually ready From 379c171d13c21d85f94b8caeac319e339cc742a3 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Sat, 15 Aug 2020 09:09:38 +0800 Subject: [PATCH 56/74] [not sure] remove step(None) in close --- tianshou/env/venvs.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 3fd9b7fe7..df4181e43 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -236,13 +236,6 @@ def close(self) -> None: ``close`` of all workers can be assured. """ self._assert_is_not_closed() - if self.is_async: - try: - # finish remaining steps, and close - if len(self.waiting_conn) > 0: - self.step(None) - except TypeError: # self.step -> self.worker.wait doesn't exist - pass for w in self.workers: w.close() self.is_closed = True From 1c9838c4abdb476d30c1d144ea2840fe367c5884 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sat, 15 Aug 2020 12:57:22 +0800 Subject: [PATCH 57/74] simple test --- test/base/test_env.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/test/base/test_env.py b/test/base/test_env.py index ba1eceb95..5773e6ec6 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -29,9 +29,8 @@ def recurse_comp(a, b): return False -def test_async_env(num=8, sleep=0.1): +def test_async_env(size=10000, num=8, sleep=0.1): # simplify the test case, just keep stepping - size = 10000 env_fns = [ lambda i=i: MyTestEnv(size=i, sleep=sleep, random_sleep=True) for i in range(size, size + num) @@ -72,6 +71,30 @@ def test_async_env(num=8, sleep=0.1): return spent_time, data +def test_async_check_id(size=10000, num=4, sleep=.1, timeout=.35): + local = __name__ == '__main__' + env_fns = [lambda: MyTestEnv(size=size, sleep=sleep * 2), + lambda: MyTestEnv(size=size, sleep=sleep * 3), + lambda: MyTestEnv(size=size, sleep=sleep * 5), + lambda: MyTestEnv(size=size, sleep=sleep * 7)] + v = SubprocVectorEnv(env_fns, wait_num=num - 1, timeout=timeout) + v.reset() + ids = np.arange(num) + act = [1] * num + t = time.time() + _, _, _, info = v.step([1] * len(ids), ids) + ids = Batch(info).env_id + print(ids, time.time() - t) + assert np.allclose(sorted(ids), [0, 1]) + assert time.time() - t > timeout + t = time.time() + _, _, _, info = v.step([1] * len(ids), ids) + ids = Batch(info).env_id + print(ids, time.time() - t) + assert np.allclose(sorted(ids), [0, 2]) + assert time.time() - t < timeout + + def test_vecenv(size=10, num=8, sleep=0.001): verbose = __name__ == '__main__' env_fns = [ @@ -124,5 +147,6 @@ def test_vecenv(size=10, num=8, sleep=0.001): if __name__ == '__main__': - test_vecenv() - test_async_env() + # test_vecenv() + # test_async_env() + test_async_check_id() From a6631bba9f5562c35db00b79a4365cb29c431a91 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Sat, 15 Aug 2020 13:29:27 +0800 Subject: [PATCH 58/74] bugfix for ray worker.wait --- tianshou/env/worker/ray.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tianshou/env/worker/ray.py b/tianshou/env/worker/ray.py index 544718b98..f9f4fa9ff 100644 --- a/tianshou/env/worker/ray.py +++ b/tianshou/env/worker/ray.py @@ -27,9 +27,10 @@ def reset(self) -> Any: def wait(workers: List['RayEnvWorker'], wait_num: int, timeout: Optional[float] = None) -> List['RayEnvWorker']: - ready_envs, _ = ray.wait([x.env for x in workers], - num_returns=wait_num, timeout=timeout) - return [workers[ready_envs.index(env)] for env in ready_envs] + results = [x.result for x in workers] + ready_results, _ = ray.wait(results, + num_returns=wait_num, timeout=timeout) + return [workers[results.index(result)] for result in ready_results] def send_action(self, action: np.ndarray) -> None: # self.action is actually a handle From 038104c4587aad2d7bc20155771a1423dd8bc0e8 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Sat, 15 Aug 2020 13:51:37 +0800 Subject: [PATCH 59/74] bugfix: timeout not passed to venv --- tianshou/env/venvs.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index df4181e43..44dd38368 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -257,7 +257,8 @@ class DummyVectorEnv(BaseVectorEnv): def __init__(self, env_fns: List[Callable[[], gym.Env]], wait_num: Optional[int] = None, timeout: Optional[float] = None) -> None: - super().__init__(env_fns, DummyEnvWorker, wait_num=wait_num) + super().__init__(env_fns, DummyEnvWorker, + wait_num=wait_num, timeout=timeout) class VectorEnv(DummyVectorEnv): @@ -282,7 +283,8 @@ def __init__(self, env_fns: List[Callable[[], gym.Env]], timeout: Optional[float] = None) -> None: def worker_fn(fn): return SubprocEnvWorker(fn, share_memory=False) - super().__init__(env_fns, worker_fn, wait_num=wait_num) + super().__init__(env_fns, worker_fn, + wait_num=wait_num, timeout=timeout) class ShmemVectorEnv(BaseVectorEnv): @@ -301,7 +303,8 @@ def __init__(self, env_fns: List[Callable[[], gym.Env]], timeout: Optional[float] = None) -> None: def worker_fn(fn): return SubprocEnvWorker(fn, share_memory=True) - super().__init__(env_fns, worker_fn, wait_num=wait_num) + super().__init__(env_fns, worker_fn, + wait_num=wait_num, timeout=timeout) class RayVectorEnv(BaseVectorEnv): @@ -326,4 +329,5 @@ def __init__(self, env_fns: List[Callable[[], gym.Env]], ) from e if not ray.is_initialized(): ray.init() - super().__init__(env_fns, RayEnvWorker, wait_num=wait_num) + super().__init__(env_fns, RayEnvWorker, + wait_num=wait_num, timeout=timeout) From 2e9cc532092018e99c765689ba2ff3e1c34aeece Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sat, 15 Aug 2020 14:46:10 +0800 Subject: [PATCH 60/74] fix test --- test/base/test_env.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/test/base/test_env.py b/test/base/test_env.py index 5773e6ec6..909e170ee 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -71,28 +71,30 @@ def test_async_env(size=10000, num=8, sleep=0.1): return spent_time, data -def test_async_check_id(size=10000, num=4, sleep=.1, timeout=.35): - local = __name__ == '__main__' +def test_async_check_id(size=100, num=4, sleep=.1, timeout=.35): env_fns = [lambda: MyTestEnv(size=size, sleep=sleep * 2), lambda: MyTestEnv(size=size, sleep=sleep * 3), lambda: MyTestEnv(size=size, sleep=sleep * 5), lambda: MyTestEnv(size=size, sleep=sleep * 7)] v = SubprocVectorEnv(env_fns, wait_num=num - 1, timeout=timeout) v.reset() + expect_result = [ + [0, 1], + [0, 1, 2], + [0, 1, 3], + [0, 1, 2], + [0, 1], + [0, 2, 3], + [0, 1], + ] ids = np.arange(num) - act = [1] * num - t = time.time() - _, _, _, info = v.step([1] * len(ids), ids) - ids = Batch(info).env_id - print(ids, time.time() - t) - assert np.allclose(sorted(ids), [0, 1]) - assert time.time() - t > timeout - t = time.time() - _, _, _, info = v.step([1] * len(ids), ids) - ids = Batch(info).env_id - print(ids, time.time() - t) - assert np.allclose(sorted(ids), [0, 2]) - assert time.time() - t < timeout + for res in expect_result: + t = time.time() + _, _, _, info = v.step([1] * len(ids), ids) + ids = Batch(info).env_id + print(ids, time.time() - t) + assert np.allclose(sorted(ids), res) + assert (time.time() - t < timeout) == (len(res) == 3) def test_vecenv(size=10, num=8, sleep=0.001): @@ -147,6 +149,6 @@ def test_vecenv(size=10, num=8, sleep=0.001): if __name__ == '__main__': - # test_vecenv() - # test_async_env() + test_vecenv() + test_async_env() test_async_check_id() From 579280f31d17cc9cf304e7129f57c37e78112d1c Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sat, 15 Aug 2020 15:36:45 +0800 Subject: [PATCH 61/74] add ray test to dev --- setup.py | 1 + test/base/test_env.py | 158 +++++++++++++++++++++++------------------- 2 files changed, 86 insertions(+), 73 deletions(-) diff --git a/setup.py b/setup.py index d1d112782..64aac40b2 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ 'flake8', 'pytest', 'pytest-cov', + 'ray>=0.8.0', ], 'atari': [ 'atari_py', diff --git a/test/base/test_env.py b/test/base/test_env.py index 909e170ee..555990e06 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -11,6 +11,14 @@ from test.base.env import MyTestEnv +def has_ray(): + try: + import ray + return hasattr(ray, 'init') + except ImportError: + return False + + def recurse_comp(a, b): try: if isinstance(a, np.ndarray): @@ -35,70 +43,76 @@ def test_async_env(size=10000, num=8, sleep=0.1): lambda i=i: MyTestEnv(size=i, sleep=sleep, random_sleep=True) for i in range(size, size + num) ] - v = SubprocVectorEnv(env_fns, wait_num=num // 2, timeout=1e-3) - v.seed() - v.reset() - # for a random variable u ~ U[0, 1], let v = max{u1, u2, ..., un} - # P(v <= x) = x^n (0 <= x <= 1), pdf of v is nx^{n-1} - # expectation of v is n / (n + 1) - # for a synchronous environment, the following actions should take - # about 7 * sleep * num / (num + 1) seconds - # for async simulation, the analysis is complicated, but the time cost - # should be smaller - action_list = [1] * num + [0] * (num * 2) + [1] * (num * 4) - current_index_start = 0 - action = action_list[:num] - env_ids = list(range(num)) - o = [] - spent_time = time.time() - while current_index_start < len(action_list): - A, B, C, D = v.step(action=action, id=env_ids) - b = Batch({'obs': A, 'rew': B, 'done': C, 'info': D}) - env_ids = b.info.env_id - o.append(b) - current_index_start += len(action) - # len of action may be smaller than len(A) in the end - action = action_list[current_index_start: current_index_start + len(A)] - # truncate env_ids with the first terms - # typically len(env_ids) == len(A) == len(action), except for the - # last batch when actions are not enough - env_ids = env_ids[: len(action)] - spent_time = time.time() - spent_time - data = Batch.cat(o) - v.close() - # assure 1/7 improvement - assert spent_time < 6.0 * sleep * num / (num + 1) - return spent_time, data + test_cls = [SubprocVectorEnv] + if has_ray(): + test_cls += [RayVectorEnv] + for cls in test_cls: + v = cls(env_fns, wait_num=num // 2, timeout=1e-3) + v.reset() + # for a random variable u ~ U[0, 1], let v = max{u1, u2, ..., un} + # P(v <= x) = x^n (0 <= x <= 1), pdf of v is nx^{n-1} + # expectation of v is n / (n + 1) + # for a synchronous environment, the following actions should take + # about 7 * sleep * num / (num + 1) seconds + # for async simulation, the analysis is complicated, but the time cost + # should be smaller + action_list = [1] * num + [0] * (num * 2) + [1] * (num * 4) + current_idx_start = 0 + action = action_list[:num] + env_ids = list(range(num)) + o = [] + spent_time = time.time() + while current_idx_start < len(action_list): + A, B, C, D = v.step(action=action, id=env_ids) + b = Batch({'obs': A, 'rew': B, 'done': C, 'info': D}) + env_ids = b.info.env_id + o.append(b) + current_idx_start += len(action) + # len of action may be smaller than len(A) in the end + action = action_list[current_idx_start:current_idx_start + len(A)] + # truncate env_ids with the first terms + # typically len(env_ids) == len(A) == len(action), except for the + # last batch when actions are not enough + env_ids = env_ids[: len(action)] + spent_time = time.time() - spent_time + Batch.cat(o) + v.close() + # assure 1/7 improvement + assert spent_time < 6.0 * sleep * num / (num + 1) -def test_async_check_id(size=100, num=4, sleep=.1, timeout=.35): +def test_async_check_id(size=100, num=4, sleep=.2, timeout=.7): env_fns = [lambda: MyTestEnv(size=size, sleep=sleep * 2), lambda: MyTestEnv(size=size, sleep=sleep * 3), lambda: MyTestEnv(size=size, sleep=sleep * 5), lambda: MyTestEnv(size=size, sleep=sleep * 7)] - v = SubprocVectorEnv(env_fns, wait_num=num - 1, timeout=timeout) - v.reset() - expect_result = [ - [0, 1], - [0, 1, 2], - [0, 1, 3], - [0, 1, 2], - [0, 1], - [0, 2, 3], - [0, 1], - ] - ids = np.arange(num) - for res in expect_result: - t = time.time() - _, _, _, info = v.step([1] * len(ids), ids) - ids = Batch(info).env_id - print(ids, time.time() - t) - assert np.allclose(sorted(ids), res) - assert (time.time() - t < timeout) == (len(res) == 3) + test_cls = [SubprocVectorEnv] + if has_ray(): + test_cls += [RayVectorEnv] + for cls in test_cls: + v = cls(env_fns, wait_num=num - 1, timeout=timeout) + v.reset() + expect_result = [ + [0, 1], + [0, 1, 2], + [0, 1, 3], + [0, 1, 2], + [0, 1], + [0, 2, 3], + [0, 1], + ] + ids = np.arange(num) + for res in expect_result: + t = time.time() + _, _, _, info = v.step([1] * len(ids), ids) + ids = Batch(info).env_id + print(ids, time.time() - t) + if cls == SubprocVectorEnv: + assert np.allclose(sorted(ids), res) + assert (time.time() - t < timeout) == (len(res) == num - 1) def test_vecenv(size=10, num=8, sleep=0.001): - verbose = __name__ == '__main__' env_fns = [ lambda i=i: MyTestEnv(size=i, sleep=sleep, recurse_state=True) for i in range(size, size + num) @@ -108,26 +122,25 @@ def test_vecenv(size=10, num=8, sleep=0.001): SubprocVectorEnv(env_fns), ShmemVectorEnv(env_fns), ] - if verbose: - venv.append(RayVectorEnv(env_fns)) + if has_ray(): + venv += [RayVectorEnv(env_fns)] for v in venv: v.seed(0) action_list = [1] * 5 + [0] * 10 + [1] * 20 - if not verbose: - o = [v.reset() for v in venv] - for i, a in enumerate(action_list): - o = [] - for v in venv: - A, B, C, D = v.step([a] * num) - if sum(C): - A = v.reset(np.where(C)[0]) - o.append([A, B, C, D]) - for index, infos in enumerate(zip(*o)): - if index == 3: # do not check info here - continue - for info in infos: - assert recurse_comp(infos[0], info) - else: + o = [v.reset() for v in venv] + for i, a in enumerate(action_list): + o = [] + for v in venv: + A, B, C, D = v.step([a] * num) + if sum(C): + A = v.reset(np.where(C)[0]) + o.append([A, B, C, D]) + for index, infos in enumerate(zip(*o)): + if index == 3: # do not check info here + continue + for info in infos: + assert recurse_comp(infos[0], info) + if __name__ == '__main__': t = [0] * len(venv) for i, e in enumerate(venv): t[i] = time.time() @@ -143,7 +156,6 @@ def test_vecenv(size=10, num=8, sleep=0.001): assert v.size == list(range(size, size + num)) assert v.env_num == num assert v.action_space == [Discrete(2)] * num - for v in venv: v.close() From 568eb833cd70e64ee5b4f41f7661d0d6d508f396 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sat, 15 Aug 2020 15:47:43 +0800 Subject: [PATCH 62/74] F401 --- test/base/test_env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/base/test_env.py b/test/base/test_env.py index 555990e06..ca0b98be7 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -14,7 +14,7 @@ def has_ray(): try: import ray - return hasattr(ray, 'init') + return hasattr(ray, 'init') # avoid PEP8 F401 Error except ImportError: return False From cb2f6ab5a754d805c175de3e387b2e44b73e95e0 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sat, 15 Aug 2020 19:23:34 +0800 Subject: [PATCH 63/74] fix getattr with reserved keys in gym.Env --- test/base/test_env.py | 2 +- tianshou/env/venvs.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/base/test_env.py b/test/base/test_env.py index ca0b98be7..c3fb7cbb0 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -107,7 +107,7 @@ def test_async_check_id(size=100, num=4, sleep=.2, timeout=.7): _, _, _, info = v.step([1] * len(ids), ids) ids = Batch(info).env_id print(ids, time.time() - t) - if cls == SubprocVectorEnv: + if cls != RayVectorEnv: assert np.allclose(sorted(ids), res) assert (time.time() - t < timeout) == (len(res) == num - 1) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 44dd38368..a101e018e 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -101,7 +101,8 @@ def __getattribute__(self, key: str) -> Any: """Switch between the default attribute getter or one looking at wrapped environment level depending on the key. """ - if key not in ('observation_space', 'action_space'): + if key not in ['metadata', 'reward_range', 'spec', 'action_space', + 'observation_space']: # reserved keys in gym.Env return super().__getattribute__(key) else: return self.__getattr__(key) From 5bf96bd7b027ec17c817d961397fc31f498b3c6a Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sat, 15 Aug 2020 19:27:10 +0800 Subject: [PATCH 64/74] reverse --- tianshou/env/venvs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index a101e018e..d39d8729d 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -101,11 +101,11 @@ def __getattribute__(self, key: str) -> Any: """Switch between the default attribute getter or one looking at wrapped environment level depending on the key. """ - if key not in ['metadata', 'reward_range', 'spec', 'action_space', - 'observation_space']: # reserved keys in gym.Env - return super().__getattribute__(key) - else: + if key in ['metadata', 'reward_range', 'spec', 'action_space', + 'observation_space']: # reserved keys in gym.Env return self.__getattr__(key) + else: + return super().__getattribute__(key) def __getattr__(self, key: str) -> Any: """Try to retrieve an attribute from each individual wrapped From bafe1bfd54a232f6abbae1335c5924f9b1a589a8 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Sat, 15 Aug 2020 19:44:08 +0800 Subject: [PATCH 65/74] doc update and EnvWorker.__getattribute__ --- tianshou/env/venvs.py | 8 +++++--- tianshou/env/worker/base.py | 14 ++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index d39d8729d..6c7458faf 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -98,8 +98,10 @@ def __len__(self) -> int: return self.env_num def __getattribute__(self, key: str) -> Any: - """Switch between the default attribute getter or one looking at - wrapped environment level depending on the key. + """Any class who inherits ``gym.Env`` will inherit some attributes, + like ``action_space``. However, we would like the attribute lookup to + go straight into the worker (in fact, this vector env's action_space + is always None). """ if key in ['metadata', 'reward_range', 'spec', 'action_space', 'observation_space']: # reserved keys in gym.Env @@ -112,7 +114,7 @@ def __getattr__(self, key: str) -> Any: environment, if it does not belong to the wrapping vector environment class. """ - return [getattr(worker, key) for worker in self.workers] + return [worker.__getattr__(key) for worker in self.workers] def _wrap_id( self, id: Optional[Union[int, List[int]]] = None) -> List[int]: diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 8dd153389..2c3c08404 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -11,11 +11,17 @@ def __init__(self, env_fn: Callable[[], gym.Env]) -> None: self._env_fn = env_fn self.is_closed = False - def __getattribute__(self, key: str): - if key not in ('observation_space', 'action_space'): - return super().__getattribute__(key) - else: + def __getattribute__(self, key: str) -> Any: + """Any class who inherits ``gym.Env`` will inherit some attributes, + like ``action_space``. However, we would like the attribute lookup to + go straight into the worker (in fact, this vector env's action_space + is always None). + """ + if key in ['metadata', 'reward_range', 'spec', 'action_space', + 'observation_space']: # reserved keys in gym.Env return self.__getattr__(key) + else: + return super().__getattribute__(key) @abstractmethod def __getattr__(self, key: str) -> Any: From 7d442f320b2f74fd1367c62183863af8ea405abd Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sat, 15 Aug 2020 19:51:36 +0800 Subject: [PATCH 66/74] polish --- tianshou/env/venvs.py | 2 +- tianshou/env/worker/base.py | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index 6c7458faf..d7ac65613 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -101,7 +101,7 @@ def __getattribute__(self, key: str) -> Any: """Any class who inherits ``gym.Env`` will inherit some attributes, like ``action_space``. However, we would like the attribute lookup to go straight into the worker (in fact, this vector env's action_space - is always None). + is always ``None``). """ if key in ['metadata', 'reward_range', 'spec', 'action_space', 'observation_space']: # reserved keys in gym.Env diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 2c3c08404..42e30b7df 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -11,18 +11,6 @@ def __init__(self, env_fn: Callable[[], gym.Env]) -> None: self._env_fn = env_fn self.is_closed = False - def __getattribute__(self, key: str) -> Any: - """Any class who inherits ``gym.Env`` will inherit some attributes, - like ``action_space``. However, we would like the attribute lookup to - go straight into the worker (in fact, this vector env's action_space - is always None). - """ - if key in ['metadata', 'reward_range', 'spec', 'action_space', - 'observation_space']: # reserved keys in gym.Env - return self.__getattr__(key) - else: - return super().__getattribute__(key) - @abstractmethod def __getattr__(self, key: str) -> Any: pass From e1da7f013cfd48d22d9c091b3dfb92b940e456b3 Mon Sep 17 00:00:00 2001 From: youkaichao Date: Sun, 16 Aug 2020 15:07:10 +0800 Subject: [PATCH 67/74] update doc for parallel simulation --- docs/tutorials/cheatsheet.rst | 43 ++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/docs/tutorials/cheatsheet.rst b/docs/tutorials/cheatsheet.rst index f9c90f290..ab0864a1e 100644 --- a/docs/tutorials/cheatsheet.rst +++ b/docs/tutorials/cheatsheet.rst @@ -31,31 +31,48 @@ See :ref:`customized_trainer`. Parallel Sampling ----------------- -Use :class:`~tianshou.env.DummyVectorEnv`, :class:`~tianshou.env.SubprocVectorEnv`, :class:`~tianshou.env.ShmemVectorEnv`, or :class:`~tianshou.env.RayVectorEnv`. -:: +Tianshou provides the following classes for parallel environment simulation: + +- :class:`~tianshou.env.DummyVectorEnv` is for pseudo-parallel simulation (implemented with a for-loop, useful for debugging). + +- :class:`~tianshou.env.SubprocVectorEnv` uses multiple processes for parallel simulation. This is the default choice for parallel simulation. + +- :class:`~tianshou.env.ShmemVectorEnv` has a similar implementation to :class:`~tianshou.env.SubprocVectorEnv`, but is optimized (in terms of both memory footprint and simulation speed) for environments with large observations such as images. - env_fns = [ - lambda: MyTestEnv(size=2), - lambda: MyTestEnv(size=3), - lambda: MyTestEnv(size=4), - lambda: MyTestEnv(size=5), - ] - venv = SubprocVectorEnv(env_fns) +- :class:`~tianshou.env.RayVectorEnv` is the only choice for parallel simulation in a cluster with multiple machines. + +Although these classes are optimized for different scenarios, they have exactly the same APIs because they are sub-classes of :class:`~tianshou.env.BaseVectorEnv`. Just provide a list of functions who return environments upon called, and it is all set. -where ``env_fns`` is a list of callable env hooker. The above code can be written in for-loop as well: :: env_fns = [lambda x=i: MyTestEnv(size=x) for i in [2, 3, 4, 5]] - venv = SubprocVectorEnv(env_fns) + venv = SubprocVectorEnv(env_fns) # DummyVectorEnv, ShmemVectorEnv, or RayVectorEnv, whichever you like. + venv.reset() # returns the initial observations of each environment + venv.step(actions) # provide actions for each environment and get their results -.. sidebar:: An example of sync/async VectorEnv (same color means the same wait step (same batch forward)) +.. sidebar:: An example of sync/async VectorEnv (steps with the same color end up in one batch that is disposed by the policy at the same time). .. Figure:: ../_static/images/async.png -All subclasses of :class:`~tianshou.env.BaseVectorEnv` have an async mode (related to `Issue 103 `_), where we can give it two extra parameters ``wait_num`` or ``timeout`` (or both). If we have 4 envs and set ``wait_num = 3``, each of the step in VectorEnv only returns 3 results of these 4 envs. This mode eases the case where each step cost varies at different timescale, e.g. 90% step cost 1s, but 10% cost 10s. +By default, parallel environment simulation is synchronous: a step is done after all environments have finished a step. Synchronous simulation works well if each step of environments costs roughly the same time. + +In case the time cost of environments varies a lot (e.g. 90% step cost 1s, but 10% cost 10s) where slow environments lag fast environments behind, async simulation can be used (related to `Issue 103 `_). The idea is to start those finished environments without waiting for slow environments. + +Asynchronous simulation is a built-in functionality of :class:`~tianshou.env.BaseVectorEnv`. Just provide ``wait_num`` or ``timeout`` (or both) and async simulation works. + +:: + + env_fns = [lambda x=i: MyTestEnv(size=x) for i in [2, 3, 4, 5]] + venv = SubprocVectorEnv(env_fns, wait_num=3, timeout=0.2) # DummyVectorEnv, ShmemVectorEnv, or RayVectorEnv, whichever you like. + venv.reset() # returns the initial observations of each environment + venv.step(actions) # returns ``wait_num`` steps or finished steps after ``timeout`` seconds + +If we have 4 envs and set ``wait_num = 3``, each of the step only returns 3 results of these 4 envs. You can treat the ``timeout`` parameter as a dynamic ``wait_num``. In each vectorized step it only returns the environments finished within the given time. If there is no such environment, it will wait until any of them finished. +The figure in the right gives an intuitive comparison among synchronous/asynchronous simulation. + .. warning:: If you use your own environment, please make sure the ``seed`` method is set up properly, e.g., From 5ce2ae4c4edfb11911f94cd25c8b1e03dcf79487 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Sun, 16 Aug 2020 17:04:43 +0800 Subject: [PATCH 68/74] docs --- README.md | 2 +- docs/tutorials/cheatsheet.rst | 21 ++++++++++++--------- docs/tutorials/dqn.rst | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f196d7099..1764051cd 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Here is Tianshou's other features: - Elegant framework, using only ~2000 lines of code -- Support (asynchronous) parallel environment sampling for all algorithms [Usage](https://tianshou.readthedocs.io/en/latest/tutorials/cheatsheet.html#parallel-sampling) +- Support parallel environment simulation (synchronous or asynchronous) for all algorithms [Usage](https://tianshou.readthedocs.io/en/latest/tutorials/cheatsheet.html#parallel-sampling) - Support recurrent state representation in actor network and critic network (RNN-style training for POMDP) [Usage](https://tianshou.readthedocs.io/en/latest/tutorials/cheatsheet.html#rnn-style-training) - Support any type of environment state (e.g. a dict, a self-defined class, ...) [Usage](https://tianshou.readthedocs.io/en/latest/tutorials/cheatsheet.html#user-defined-environment-and-different-state-representation) - Support customized training process [Usage](https://tianshou.readthedocs.io/en/latest/tutorials/cheatsheet.html#customize-training-process) diff --git a/docs/tutorials/cheatsheet.rst b/docs/tutorials/cheatsheet.rst index ab0864a1e..0f42f8198 100644 --- a/docs/tutorials/cheatsheet.rst +++ b/docs/tutorials/cheatsheet.rst @@ -35,20 +35,20 @@ Tianshou provides the following classes for parallel environment simulation: - :class:`~tianshou.env.DummyVectorEnv` is for pseudo-parallel simulation (implemented with a for-loop, useful for debugging). -- :class:`~tianshou.env.SubprocVectorEnv` uses multiple processes for parallel simulation. This is the default choice for parallel simulation. +- :class:`~tianshou.env.SubprocVectorEnv` uses multiple processes for parallel simulation. This is the most often choice for parallel simulation. - :class:`~tianshou.env.ShmemVectorEnv` has a similar implementation to :class:`~tianshou.env.SubprocVectorEnv`, but is optimized (in terms of both memory footprint and simulation speed) for environments with large observations such as images. -- :class:`~tianshou.env.RayVectorEnv` is the only choice for parallel simulation in a cluster with multiple machines. +- :class:`~tianshou.env.RayVectorEnv` is currently the only choice for parallel simulation in a cluster with multiple machines. Although these classes are optimized for different scenarios, they have exactly the same APIs because they are sub-classes of :class:`~tianshou.env.BaseVectorEnv`. Just provide a list of functions who return environments upon called, and it is all set. :: env_fns = [lambda x=i: MyTestEnv(size=x) for i in [2, 3, 4, 5]] - venv = SubprocVectorEnv(env_fns) # DummyVectorEnv, ShmemVectorEnv, or RayVectorEnv, whichever you like. - venv.reset() # returns the initial observations of each environment - venv.step(actions) # provide actions for each environment and get their results + venv = SubprocVectorEnv(env_fns) # DummyVectorEnv, ShmemVectorEnv, or RayVectorEnv, whichever you like. + venv.reset() # returns the initial observations of each environment + venv.step(actions) # provide actions for each environment and get their results .. sidebar:: An example of sync/async VectorEnv (steps with the same color end up in one batch that is disposed by the policy at the same time). @@ -62,10 +62,13 @@ Asynchronous simulation is a built-in functionality of :class:`~tianshou.env.Bas :: - env_fns = [lambda x=i: MyTestEnv(size=x) for i in [2, 3, 4, 5]] - venv = SubprocVectorEnv(env_fns, wait_num=3, timeout=0.2) # DummyVectorEnv, ShmemVectorEnv, or RayVectorEnv, whichever you like. - venv.reset() # returns the initial observations of each environment - venv.step(actions) # returns ``wait_num`` steps or finished steps after ``timeout`` seconds + env_fns = [lambda x=i: MyTestEnv(size=x, sleep=x) for i in [2, 3, 4, 5]] + # DummyVectorEnv, ShmemVectorEnv, or RayVectorEnv, whichever you like. + venv = SubprocVectorEnv(env_fns, wait_num=3, timeout=0.2) + venv.reset() # returns the initial observations of each environment + # returns ``wait_num`` steps or finished steps after ``timeout`` seconds, + # whichever occurs first. + venv.step(actions, ready_id) If we have 4 envs and set ``wait_num = 3``, each of the step only returns 3 results of these 4 envs. diff --git a/docs/tutorials/dqn.rst b/docs/tutorials/dqn.rst index 74a40fbb9..e01760058 100644 --- a/docs/tutorials/dqn.rst +++ b/docs/tutorials/dqn.rst @@ -30,7 +30,7 @@ It is available if you want the original ``gym.Env``: train_envs = gym.make('CartPole-v0') test_envs = gym.make('CartPole-v0') -Tianshou supports parallel sampling for all algorithms. It provides four types of vectorized environment wrapper: :class:`~tianshou.env.DummyVectorEnv`, :class:`~tianshou.env.SubprocVectorEnv`, :class:`~tianshou.env.ShmemVectorEnv`, and :class:`~tianshou.env.RayVectorEnv`. It can be used as follows: +Tianshou supports parallel sampling for all algorithms. It provides four types of vectorized environment wrapper: :class:`~tianshou.env.DummyVectorEnv`, :class:`~tianshou.env.SubprocVectorEnv`, :class:`~tianshou.env.ShmemVectorEnv`, and :class:`~tianshou.env.RayVectorEnv`. It can be used as follows: (more explanation can be found at :ref:`parallel_sampling`) :: train_envs = ts.env.DummyVectorEnv([lambda: gym.make('CartPole-v0') for _ in range(8)]) From 82ead961ac4a372787943c79e08470d09ec53cf7 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Mon, 17 Aug 2020 08:11:12 +0800 Subject: [PATCH 69/74] add warning instead of remove --- tianshou/data/collector.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tianshou/data/collector.py b/tianshou/data/collector.py index 2bda0ed76..268792a24 100644 --- a/tianshou/data/collector.py +++ b/tianshou/data/collector.py @@ -349,7 +349,6 @@ def sample(self, batch_size: int) -> Batch: the buffer, otherwise it will extract the data with the given batch_size. """ - import warnings warnings.warn( 'Collector.sample is deprecated and will cause error if you use ' 'prioritized experience replay! Collector.sample will be removed ' @@ -358,6 +357,11 @@ def sample(self, batch_size: int) -> Batch: batch_data = self.process_fn(batch_data, self.buffer, indice) return batch_data + def close(self) -> None: + warnings.warn( + 'Collector.close is deprecated and will be removed upon version ' + '0.3.', Warning) + def _batch_set_item(source: Batch, indices: np.ndarray, target: Batch, size: int): From e7372ec40a1fc1a90919f3415c77b3b674ccae5e Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Mon, 17 Aug 2020 08:11:55 +0800 Subject: [PATCH 70/74] 0.2.6 --- tianshou/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tianshou/__init__.py b/tianshou/__init__.py index e70b00237..d73c93fc0 100644 --- a/tianshou/__init__.py +++ b/tianshou/__init__.py @@ -1,7 +1,7 @@ from tianshou import data, env, utils, policy, trainer, \ exploration -__version__ = '0.2.5' +__version__ = '0.2.6' __all__ = [ 'env', 'data', From cee76e44dc77ac3a5caa8439a9320c6aab41a1ec Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Mon, 17 Aug 2020 08:28:30 +0800 Subject: [PATCH 71/74] stable test async --- test/base/test_env.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/base/test_env.py b/test/base/test_env.py index c3fb7cbb0..d0a084dd0 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -105,11 +105,12 @@ def test_async_check_id(size=100, num=4, sleep=.2, timeout=.7): for res in expect_result: t = time.time() _, _, _, info = v.step([1] * len(ids), ids) + t = time.time() - t ids = Batch(info).env_id - print(ids, time.time() - t) - if cls != RayVectorEnv: + print(ids, t) + if cls != RayVectorEnv: # ray-project/ray#10134 assert np.allclose(sorted(ids), res) - assert (time.time() - t < timeout) == (len(res) == num - 1) + assert (t < timeout) == (len(res) == num - 1) def test_vecenv(size=10, num=8, sleep=0.001): From ba77aacf0cac4a9344d9535617dec60f46658713 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Tue, 18 Aug 2020 15:16:59 +0800 Subject: [PATCH 72/74] remove gym.Env from BaseWorker --- tianshou/env/venvs.py | 2 +- tianshou/env/worker/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tianshou/env/venvs.py b/tianshou/env/venvs.py index d7ac65613..504d3e196 100644 --- a/tianshou/env/venvs.py +++ b/tianshou/env/venvs.py @@ -114,7 +114,7 @@ def __getattr__(self, key: str) -> Any: environment, if it does not belong to the wrapping vector environment class. """ - return [worker.__getattr__(key) for worker in self.workers] + return [getattr(worker, key) for worker in self.workers] def _wrap_id( self, id: Optional[Union[int, List[int]]] = None) -> List[int]: diff --git a/tianshou/env/worker/base.py b/tianshou/env/worker/base.py index 42e30b7df..87fb6c2e8 100644 --- a/tianshou/env/worker/base.py +++ b/tianshou/env/worker/base.py @@ -4,7 +4,7 @@ from typing import List, Tuple, Optional, Callable, Any -class EnvWorker(ABC, gym.Env): +class EnvWorker(ABC): """An abstract worker for an environment.""" def __init__(self, env_fn: Callable[[], gym.Env]) -> None: From 2080e6a63f330dbd825e8d2251bc46de62225ce9 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 19 Aug 2020 10:55:56 +0800 Subject: [PATCH 73/74] change to parser.parse_args() --- docs/tutorials/tictactoe.rst | 3 +-- examples/atari/pong_a2c.py | 3 +-- examples/atari/pong_dqn.py | 3 +-- examples/atari/pong_ppo.py | 3 +-- examples/box2d/acrobot_dualdqn.py | 3 +-- examples/box2d/bipedal_hardcore_sac.py | 3 +-- examples/box2d/lunarlander_dqn.py | 3 +-- examples/box2d/sac_mcc.py | 3 +-- examples/mujoco/ant_v2_ddpg.py | 3 +-- examples/mujoco/ant_v2_sac.py | 3 +-- examples/mujoco/ant_v2_td3.py | 3 +-- examples/mujoco/halfcheetahBullet_v0_sac.py | 3 +-- examples/mujoco/point_maze_td3.py | 4 +--- 13 files changed, 13 insertions(+), 27 deletions(-) diff --git a/docs/tutorials/tictactoe.rst b/docs/tutorials/tictactoe.rst index be58771ba..6ab79d800 100644 --- a/docs/tutorials/tictactoe.rst +++ b/docs/tutorials/tictactoe.rst @@ -219,8 +219,7 @@ The explanation of each Tianshou class/function will be deferred to their first help='the path of opponent agent pth file for resuming from a pre-trained agent') parser.add_argument('--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu') - args = parser.parse_known_args()[0] - return args + return parser.parse_args() .. sidebar:: The relationship between MultiAgentPolicyManager (Manager) and BasePolicy (Agent) diff --git a/examples/atari/pong_a2c.py b/examples/atari/pong_a2c.py index bd749fcea..f4b0a3031 100644 --- a/examples/atari/pong_a2c.py +++ b/examples/atari/pong_a2c.py @@ -40,8 +40,7 @@ def get_args(): parser.add_argument('--ent-coef', type=float, default=0.001) parser.add_argument('--max-grad-norm', type=float, default=None) parser.add_argument('--max_episode_steps', type=int, default=2000) - args = parser.parse_known_args()[0] - return args + return parser.parse_args() def test_a2c(args=get_args()): diff --git a/examples/atari/pong_dqn.py b/examples/atari/pong_dqn.py index da31ecdb7..6dda89400 100644 --- a/examples/atari/pong_dqn.py +++ b/examples/atari/pong_dqn.py @@ -36,8 +36,7 @@ def get_args(): parser.add_argument( '--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu') - args = parser.parse_known_args()[0] - return args + return parser.parse_args() def test_dqn(args=get_args()): diff --git a/examples/atari/pong_ppo.py b/examples/atari/pong_ppo.py index 9d6ede8ad..9d5563fe1 100644 --- a/examples/atari/pong_ppo.py +++ b/examples/atari/pong_ppo.py @@ -40,8 +40,7 @@ def get_args(): parser.add_argument('--eps-clip', type=float, default=0.2) parser.add_argument('--max-grad-norm', type=float, default=0.5) parser.add_argument('--max_episode_steps', type=int, default=2000) - args = parser.parse_known_args()[0] - return args + return parser.parse_args() def test_ppo(args=get_args()): diff --git a/examples/box2d/acrobot_dualdqn.py b/examples/box2d/acrobot_dualdqn.py index 9ae679262..e3de12de7 100644 --- a/examples/box2d/acrobot_dualdqn.py +++ b/examples/box2d/acrobot_dualdqn.py @@ -36,8 +36,7 @@ def get_args(): parser.add_argument( '--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu') - args = parser.parse_known_args()[0] - return args + return parser.parse_args() def test_dqn(args=get_args()): diff --git a/examples/box2d/bipedal_hardcore_sac.py b/examples/box2d/bipedal_hardcore_sac.py index 7eb906e28..31b83f43b 100644 --- a/examples/box2d/bipedal_hardcore_sac.py +++ b/examples/box2d/bipedal_hardcore_sac.py @@ -39,8 +39,7 @@ def get_args(): parser.add_argument( '--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu') - args = parser.parse_known_args()[0] - return args + return parser.parse_args() class EnvWrapper(object): diff --git a/examples/box2d/lunarlander_dqn.py b/examples/box2d/lunarlander_dqn.py index c81082869..0e66c65f7 100644 --- a/examples/box2d/lunarlander_dqn.py +++ b/examples/box2d/lunarlander_dqn.py @@ -37,8 +37,7 @@ def get_args(): parser.add_argument( '--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu') - args = parser.parse_known_args()[0] - return args + return parser.parse_args() def test_dqn(args=get_args()): diff --git a/examples/box2d/sac_mcc.py b/examples/box2d/sac_mcc.py index 527800543..845ffcd7b 100644 --- a/examples/box2d/sac_mcc.py +++ b/examples/box2d/sac_mcc.py @@ -41,8 +41,7 @@ def get_args(): parser.add_argument( '--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu') - args = parser.parse_known_args()[0] - return args + return parser.parse_args() def test_sac(args=get_args()): diff --git a/examples/mujoco/ant_v2_ddpg.py b/examples/mujoco/ant_v2_ddpg.py index 3d287c5e7..ef7ea6c42 100644 --- a/examples/mujoco/ant_v2_ddpg.py +++ b/examples/mujoco/ant_v2_ddpg.py @@ -36,8 +36,7 @@ def get_args(): parser.add_argument( '--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu') - args = parser.parse_known_args()[0] - return args + return parser.parse_args() def test_ddpg(args=get_args()): diff --git a/examples/mujoco/ant_v2_sac.py b/examples/mujoco/ant_v2_sac.py index 9165b13cc..402784f28 100644 --- a/examples/mujoco/ant_v2_sac.py +++ b/examples/mujoco/ant_v2_sac.py @@ -37,8 +37,7 @@ def get_args(): parser.add_argument( '--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu') - args = parser.parse_known_args()[0] - return args + return parser.parse_args() def test_sac(args=get_args()): diff --git a/examples/mujoco/ant_v2_td3.py b/examples/mujoco/ant_v2_td3.py index 665e45874..fad3f911c 100644 --- a/examples/mujoco/ant_v2_td3.py +++ b/examples/mujoco/ant_v2_td3.py @@ -39,8 +39,7 @@ def get_args(): parser.add_argument( '--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu') - args = parser.parse_known_args()[0] - return args + return parser.parse_args() def test_td3(args=get_args()): diff --git a/examples/mujoco/halfcheetahBullet_v0_sac.py b/examples/mujoco/halfcheetahBullet_v0_sac.py index 0c947170a..8f1a103e4 100644 --- a/examples/mujoco/halfcheetahBullet_v0_sac.py +++ b/examples/mujoco/halfcheetahBullet_v0_sac.py @@ -43,8 +43,7 @@ def get_args(): parser.add_argument( '--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu') - args = parser.parse_known_args()[0] - return args + return parser.parse_args() def test_sac(args=get_args()): diff --git a/examples/mujoco/point_maze_td3.py b/examples/mujoco/point_maze_td3.py index 6478c31a9..42e91146c 100644 --- a/examples/mujoco/point_maze_td3.py +++ b/examples/mujoco/point_maze_td3.py @@ -41,9 +41,7 @@ def get_args(): '--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu') parser.add_argument('--max_episode_steps', type=int, default=2000) - - args = parser.parse_known_args()[0] - return args + return parser.parse_args() def test_td3(args=get_args()): From 091f39d3a18a909300391725ae40623b4dfcee7b Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Wed, 19 Aug 2020 14:41:34 +0800 Subject: [PATCH 74/74] move _setup_buf --- test/base/test_env.py | 6 +++--- tianshou/env/worker/subproc.py | 28 +++++++++++++--------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/test/base/test_env.py b/test/base/test_env.py index d0a084dd0..96de70236 100644 --- a/test/base/test_env.py +++ b/test/base/test_env.py @@ -3,7 +3,7 @@ from gym.spaces.discrete import Discrete from tianshou.data import Batch from tianshou.env import DummyVectorEnv, SubprocVectorEnv, \ - RayVectorEnv, ShmemVectorEnv + ShmemVectorEnv, RayVectorEnv if __name__ == '__main__': from env import MyTestEnv @@ -43,7 +43,7 @@ def test_async_env(size=10000, num=8, sleep=0.1): lambda i=i: MyTestEnv(size=i, sleep=sleep, random_sleep=True) for i in range(size, size + num) ] - test_cls = [SubprocVectorEnv] + test_cls = [SubprocVectorEnv, ShmemVectorEnv] if has_ray(): test_cls += [RayVectorEnv] for cls in test_cls: @@ -86,7 +86,7 @@ def test_async_check_id(size=100, num=4, sleep=.2, timeout=.7): lambda: MyTestEnv(size=size, sleep=sleep * 3), lambda: MyTestEnv(size=size, sleep=sleep * 5), lambda: MyTestEnv(size=size, sleep=sleep * 7)] - test_cls = [SubprocVectorEnv] + test_cls = [SubprocVectorEnv, ShmemVectorEnv] if has_ray(): test_cls += [RayVectorEnv] for cls in test_cls: diff --git a/tianshou/env/worker/subproc.py b/tianshou/env/worker/subproc.py index 29502d065..6ba108eba 100644 --- a/tianshou/env/worker/subproc.py +++ b/tianshou/env/worker/subproc.py @@ -94,6 +94,18 @@ def get(self): dtype=self.dtype).reshape(self.shape) +def _setup_buf(space): + if isinstance(space, gym.spaces.Dict): + assert isinstance(space.spaces, OrderedDict) + buffer = {k: _setup_buf(v) for k, v in space.spaces.items()} + elif isinstance(space, gym.spaces.Tuple): + assert isinstance(space.spaces, tuple) + buffer = tuple([_setup_buf(t) for t in space.spaces]) + else: + buffer = ShArray(space.dtype, space.shape) + return buffer + + class SubprocEnvWorker(EnvWorker): """Subprocess worker used in SubprocVectorEnv and ShmemVectorEnv.""" @@ -108,7 +120,7 @@ def __init__(self, env_fn: Callable[[], gym.Env], obs_space = dummy.observation_space dummy.close() del dummy - self.buffer = SubprocEnvWorker._setup_buf(obs_space) + self.buffer = _setup_buf(obs_space) args = (self.parent_remote, self.child_remote, CloudpickleWrapper(env_fn), self.buffer) self.process = Process(target=_worker, args=args, daemon=True) @@ -119,20 +131,6 @@ def __getattr__(self, key: str): self.parent_remote.send(['getattr', key]) return self.parent_remote.recv() - @staticmethod - def _setup_buf(space): - if isinstance(space, gym.spaces.Dict): - assert isinstance(space.spaces, OrderedDict) - buffer = {k: SubprocEnvWorker._setup_buf(v) - for k, v in space.spaces.items()} - elif isinstance(space, gym.spaces.Tuple): - assert isinstance(space.spaces, tuple) - buffer = tuple([SubprocEnvWorker._setup_buf(t) - for t in space.spaces]) - else: - buffer = ShArray(space.dtype, space.shape) - return buffer - def _decode_obs(self, isNone): def decode_obs(buffer): if isinstance(buffer, ShArray):

  • AmIt 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 EFJ6w5*Z=?k From c4bea74a8e3a44db37862c3e57213d3d3882d612 Mon Sep 17 00:00:00 2001 From: Trinkle23897 <463003665@qq.com> Date: Thu, 13 Aug 2020 17:49:21 +0800 Subject: [PATCH 50/74] docs --- docs/_static/images/async.png | Bin 36743 -> 54626 bytes docs/tutorials/cheatsheet.rst | 6 +++--- tianshou/env/venvs.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/_static/images/async.png b/docs/_static/images/async.png index e305044cf959f91b2a56b1379b5db2568c619500..9c86a840b7a791d2969222fd9acf9b7108f088b3 100644 GIT binary patch literal 54626 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%0DA;TSYqmPFA^r$Jh2s40(xuHaOkWq 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 literal 36743 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