diff --git a/pyrobolearn/algos/explorer.py b/pyrobolearn/algos/explorer.py index c9cbb66..b9e5619 100644 --- a/pyrobolearn/algos/explorer.py +++ b/pyrobolearn/algos/explorer.py @@ -5,16 +5,23 @@ It consists to explore and collect samples in the environment using the policy. given memory/storage unit which will be used to evaluate the policy, and then update its parameters. """ +import os +import copy import inspect +import torch +import torch.multiprocessing as multiprocessing +import torch.distributed as distributed # note that you can only send/receive tensors with P2P backends + from pyrobolearn.tasks import RLTask from pyrobolearn.envs import Env from pyrobolearn.policies import Policy from pyrobolearn.exploration import Exploration -from pyrobolearn.storages import DictStorage # RolloutStorage +from pyrobolearn.storages import RolloutStorage, ExperienceReplay from pyrobolearn import logger + __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] @@ -34,25 +41,108 @@ class Explorer(object): 2. Evaluate: Assess the quality of the actions/trajectories using the returns. 3. Update: Update the policy (and/or value function) parameters based on the loss - This class focuses on the first step of RL algorithms. It accepts the environment, and the exploration strategy - which wraps the policy. + This class focuses on the first step of RL algorithms. It accepts the task (environment and policy), the + exploration strategy which wraps the policy, and the rollout (for on-policy) or experience replay (for off-policy) + storage unit. """ - def __init__(self, task, explorer, storage, num_workers=1): + def __init__(self, task, explorer, storage, num_workers=1, backend='multiprocessing'): """ Initialize the exploration phase. Args: task (Task, Env, tuple of Env and Policy): RL task or environment. explorer (Exploration): policies. - storage (DictStorage): Rollout storage unit (=replay memory). It will save the trajectories / rollouts / - transitions in the storage while exploring. - num_workers (int): number of processes / workers to run in parallel. + storage (RolloutStorage, ExperienceReplay): Rollout (for on-policy) or experience replay (for off-policy) + storage unit (=replay memory). It will save the trajectories / rollouts / transitions in the storage + while exploring. + num_workers (int): number of processes / workers to run in parallel. This number has to be equal or smaller + than the number of CPUs on the computer. If bigger, it will automatically be clipped when using the + 'multiprocessing' backend (see below). If only :attr:`num_workers=1`, it doesn't + backend: backend to be used when using multiple processes. The different possible backends are + 'multiprocessing' (by default), 'gloo' (good for CPUs), 'nccl' (good for GPUs), 'mpi' (only valid if + PyTorch has been built from source with MPI support). For more information, we refer the reader to + references [2,4]. If the backend is 'mpi', you have to run the code using the following command: + `mpirun -n 4 python .py`. + + References: + [1] Multiprocessing best practices: https://pytorch.org/docs/stable/notes/multiprocessing.html + [2] torch multiprocessing: https://pytorch.org/docs/stable/multiprocessing.html + [3] Writing Distributed Applications with PyTorch: https://pytorch.org/tutorials/intermediate/dist_tuto.html + [4] torch distributed: https://pytorch.org/docs/stable/distributed.html """ self.task = task self.explorer = explorer self.storage = storage - self.num_workers = int(num_workers) + + # check the number of workers + if not isinstance(num_workers, (int, long)): + raise TypeError("Expecting the number of workers to be an integer, instead got: {}".format(num_workers)) + self.num_workers = 1 if num_workers < 1 else int(num_workers) + + # make sure that the maximum number of worker/process is smaller or equal to the number of CPUs + if self.num_workers > multiprocessing.cpu_count(): + self.num_workers = multiprocessing.cpu_count() + + # check backend + if backend is None: + backend = 'multiprocessing' + if not isinstance(backend, str): + raise TypeError("Expecting the given 'backend' to be a string, instead got: {}".format(type(backend))) + backend = backend.lower() + if backend not in {'multiprocessing', 'mpi', 'gloo', 'nccl'}: + raise ValueError("Expecting the given 'backend' to be 'multiprocessing', 'mpi', 'gloo', or 'nccl', instead " + "got: {}".format(backend)) + self.backend = backend + + # create processes + self.processes = [] + self.pipe = None + self.process_id = 0 # only the master should have the id set to 0, the workers have a strictly positive id + if self.num_workers > 1: + if self.backend == 'multiprocessing': + is_rendering = self.environment.is_rendering + rendering_mode = self.environment.rendering_mode + + # hide the GUI of the environment + if is_rendering: + self.environment.hide() + + # create processes + self.queue = multiprocessing.Queue() + self.pipes = [multiprocessing.Pipe() for _ in range(self.num_workers)] + self.processes = [multiprocessing.Process(target=self.explore_in_parallel, args=(pipe[1], self.queue, + task)) + for pipe in self.pipes] + + # start processes + for process in self.processes: + process.start() + + # render the GUI if it was initially rendered + if is_rendering: + self.environment.render(mode=rendering_mode) + + else: + raise NotImplementedError("Currently, other backends are not provided.") + + # elif self.backend == 'gloo' or self.backend == 'nccl': + # + # if self.backend == 'nccl' and not distributed.is_nccl_available(): + # raise ValueError("The 'nccl' backend is not available on this computer.") + # + # # create processes + # self.processes = [multiprocessing.Process(target=init_processes, args=(rank, size, function))] + # + # # start processes + # for process in self.processes: + # process.start() + # + # elif self.backend == 'mpi': + # if not distributed.is_mpi_available(): + # raise ValueError("The 'mpi' backend is not available on this computer.") + # + # init_processes(0, 0, function, self.backend) ############## # Properties # @@ -91,6 +181,12 @@ class Explorer(object): """Return the environment.""" return self.task.environment + # alias + @property + def environment(self): + """Return the environment""" + return self.task.environment + @property def explorer(self): """Return the exploration strategy.""" @@ -113,89 +209,417 @@ class Explorer(object): @storage.setter def storage(self, storage): """Set the storage unit.""" - if not isinstance(storage, DictStorage): - raise TypeError("Expecting the storage to be an instance of `DictStorage`, instead got: " - "{}".format(type(storage))) + if not isinstance(storage, (RolloutStorage, ExperienceReplay)): # DictStorage): + raise TypeError("Expecting the storage to be an instance of `RolloutStorage` or `ExperienceReplay`, " + "instead got: {}".format(type(storage))) self._storage = storage ########### # Methods # ########### - def explore(self, num_steps, rollout_idx=0, deterministic=False, verbose=False): - """ - Explore in the environment. + def close(self): + """End the processes.""" + for process in self.processes: + process.terminate() + + def rollout(self, num_steps, deterministic=False, render=False, verbose=False): + """Perform a rollout. Args: - num_steps (int): number of steps - rollout_idx (int): trajectory/rollout index. - deterministic (bool): if deterministic is True, then it does not explore in the environment. - verbose (bool): If true, print information on the standard output. - - Returns: - DictStorage: updated memory storage + num_steps (int): number of steps. + deterministic (bool): if the policy should act deterministically instead of exploring (based on the + exploration strategy). + render (bool): if we should render the environment. + verbose (bool): if we should print information about the rollout. """ # reset environment observation = self.env.reset() - if verbose: - print("\n#### Starting the Exploration phase ####") - # print("Explorer - initial state: {}".format(observation)) - - # reset storage - self.storage.reset(init_states=observation, rollout_idx=rollout_idx) # reset explorer self.explorer.reset() + if verbose: + print("Start the rollout") + # run RL task for T steps + trajectory = [] for step in range(num_steps): + # if we need to render + if render: + self.env.render() + # get action and corresponding distribution from policy action, distribution = self.explorer.act(observation, deterministic=deterministic) # perform one step in the environment next_observation, reward, done, info = self.env.step(action) + # append the transition tuple + trajectory.append({'states': observation, 'actions': action, 'next_states': next_observation, + 'reward': reward, 'mask': (1 - done), 'distribution': distribution}) + # if verbose: - # print("\nExplorer:") - # print("1. Observation data: {}".format(observation)) - # print("2. Action data: {}".format(action)) - # print("3. Next observation data: {}".format(next_observation)) - # print("4. Reward: {}".format(reward)) - # print("5. \\pi(.|s): {}".format(distribution)) - # print("6. log \\pi(a|s): {}".format([d.log_prob(action) for d in distribution])) - - # insert in storage - self.storage.insert(observation, action, next_observation, reward, mask=(1-done), - distributions=distribution, rollout_idx=rollout_idx) - - # set current observation to current one - observation = next_observation + # print("\n\t1. Observation data: {}".format(observation)) + # print("\t2. Action data: {}".format(action)) + # print("\t3. Next observation data: {}".format(next_observation)) + # print("\t4. Reward: {}".format(reward)) + # print("\t5. \\pi(.|s): {}".format(distribution)) + # print("\t6. log \\pi(a|s): {}".format([d.log_prob(action) for d in distribution])) # if done, get out of the loop if done: break - # fill remaining mask values - self.storage.end(rollout_idx) + # set current observation to current one + observation = next_observation if verbose: - print("#### End of the Exploration phase #####") - # print("states: {}".format(self.storage['states'])) - # print("actions: {}".format(self.storage['actions'])) - # print("rewards: {}".format(self.storage['rewards'])) - # print("masks: {}".format(self.storage['masks'])) - # print("distributions: {}".format(self.storage['distributions'])) + print("End of the rollout") - # # clear explorer - # self.explorer.clear() + # return trajectory + return trajectory + + def __send(self, msg, dst=0, rank=0): + """ + Send the given message to the specified destination. + + - master: send to the specified worker process the information about the rollout (number of steps, parameters, + etc). + - worker: send its rank / process id and the trajectory to the master + + Args: + msg (tuple of object): message to send. + dst (int): destination rank or process id to send the message to. The master process has a rank of 0. + rank (int): rank or process id. The master process has a rank/id of 0, while the workers have a strictly + positive integer id. + """ + if self.backend == 'multiprocessing': + + # master process: send to the specified worker process the information about the rollout (number of steps, + # parameters, etc). + if rank == 0: + self.pipes[dst][0].send(msg) + + # worker process: add the message (process id and trajectory) to the queue + else: + self.queue.put(msg) + else: + # TODO: the messages have to be tensors!! + # master process: send to the specified worker process the information about the rollout (number of steps, + # parameters, etc). + if rank == 0: + num_steps, render, verbose, parameters = msg + + distributed.send(tensor=torch.tensor(num_steps).view(1), dst=dst) + distributed.send(tensor=torch.tensor(render).view(1), dst=dst) + distributed.send(tensor=torch.tensor(verbose).view(1), dst=dst) + + for parameter in parameters: + distributed.send(tensor=parameter, dst=dst) + + # worker process: send the message (process id and trajectory) to the queue + else: + process_id, trajectory = msg + distributed.send(tensor=torch.tensor(process_id).view(1), dst=0) + + for transition in trajectory: + distributed.send(tensor=transition['states'], dst=0) + distributed.send(tensor=transition['actions'], dst=0) + distributed.send(tensor=transition['next_states'], dst=0) + distributed.send(tensor=transition['reward'], dst=0) + distributed.send(tensor=transition['mask'], dst=0) + # TODO: send distribution (its hyperparameters? because I can only send tensors... Or its entropy + # and the log likelihood of the action evaluated with the distribution?) + # distributed.send(tensor=transition['distribution'], dst=0) + + def __recv(self, rank=0): + """ + Receive the message from the master or worker process. + + - master: receive the message (i.e. the process id (or rank) and the trajectory) from the worker + - worker: receive information about the rollout (num_steps, parameters, etc) from the master + + Args: + rank (int): rank or process id. The master process has a rank/id of 0, while the workers have a strictly + positive integer id. + + Returns: + int: process id + list of torch.Tensor: the trajectory. + """ + if self.backend == 'multiprocessing': + + # master process: receive the process id and trajectory from the workers + if rank == 0: + process_id, trajectory = self.queue.get() + return process_id, trajectory + + # worker process: receive information about the rollout (num_steps, parameters, etc) from the master + msg = self.pipe.recv() + return msg + + else: + # TODO: can only receive tensors and they have to be allocated in advance with the correct dimensions!! + # master process: receive the process id and trajectory from the workers + if rank == 0: + # preallocate the tensors + scalar_tensor = torch.zeros(1) + state_shapes = self.explorer.policy.states.merged_shape + action_shapes = self.explorer.policy.actions.merged_shape + + # get process id / rank + distributed.recv(tensor=scalar_tensor, src=None) + src = int(scalar_tensor) + + # get trajectory length + distributed.recv(tensor=scalar_tensor, src=src) + trajectory_length = int(scalar_tensor) + + # copy trajectory + trajectory = [] + state_tensors = [torch.zeros(shape) for shape in state_shapes] + for i in range(trajectory_length): + # copy transition tuple + transition = {} + + # allocate tensors in advance + action_tensors = [torch.zeros(shape) for shape in action_shapes] + next_state_tensors = [torch.zeros(shape) for shape in state_shapes] + reward_tensor = torch.zeros(1) + mask_tensor = torch.zeros(1) + + # copy states/observations + for state_tensor in state_tensors: + distributed.recv(tensor=state_tensor, src=src) + transition['states'] = state_tensors + + # copy actions + for action_tensor in action_tensors: + distributed.recv(tensor=action_tensor, src=src) + transition['actions'] = action_tensors + + # copy next states/observations + for state_tensor in next_state_tensors: + distributed.recv(tensor=state_tensor, src=src) + transition['next_states'] = state_tensors + + # copy reward + distributed.recv(tensor=reward_tensor, src=src) + transition['reward'] = reward_tensor + + # copy mask/done + distributed.recv(tensor=mask_tensor, src=src) + transition['mask'] = mask_tensor + + # TODO: copy distribution + # distributed.recv(tensor=distribution_tensor, src=src) + # transition['distribution'] = distribution_tensor + + # add transition tuple in the trajectory + trajectory.append(transition) + + # set the state tensors to the next one (to be memory and time efficient) + state_tensors = next_state_tensors + + return src, trajectory + + # worker process: receive information about the rollout (num_steps, parameters, etc) from the master + # get number of steps + scalar_tensor = torch.zeros(1) + distributed.recv(tensor=scalar_tensor, src=0) + num_steps = int(scalar_tensor) + + # get if we should render or not, and verbose + distributed.recv(tensor=scalar_tensor, src=0) + render = bool(scalar_tensor) + distributed.recv(tensor=scalar_tensor, src=0) + verbose = bool(scalar_tensor) + + # copy parameters + parameters = [] + for parameter in self.explorer.policy.parameters(): + parameter = torch.zeros(parameter.shape) + distributed.recv(tensor=parameter, src=0) + parameters.append(parameter) + + return num_steps, render, verbose, parameters + + def _master_explore(self, num_steps, num_rollouts=1, render=False, verbose=False): + """Master: send the add the trajectories or transition tuples in the storage unit.""" + # create set of integers + pool = set(range(min(len(self.processes), num_rollouts))) + process, rollout = 0, 0 + while True: + if pool and process < num_rollouts: + # send job to process + process_id = pool.pop() + # self.pipes[process_id][0].send((num_steps, self.explorer)) + self.__send((num_steps, render, verbose, self.explorer.policy.parameters()), dst=process_id, rank=0) + process += 1 + + elif process >= num_rollouts and rollout >= num_rollouts: + # get out of the loop + break + + else: + # wait for result from queue + process_id, trajectory = self.__recv(rank=0) # self.queue.get() + + # put the process id in the pool to let know that it is free + pool.add(process_id) + + # add the trajectory in the storage + self.storage.add_trajectory(trajectory, rollout_idx=rollout) + rollout += 1 + + return self.storage + + def _worker_explore(self, pipe=None): + """Worker explore in the environment.""" + # get process + process = multiprocessing.current_process() + self.process_id = int(process.name.split('-')[-1]) + + if self.backend == 'multiprocessing': + self.pipe = pipe + else: + os.environ['MASTER_ADDR'] = '127.0.0.1' + os.environ['MASTER_PORT'] = '29500' + os.environ['WORLD_SIZE'] = str(self.num_workers) + os.environ['RANK'] = str(self.process_id) + distributed.init_process_group(self.backend, rank=self.process_id, world_size=self.num_workers) + + # copy task + self.task = copy.deepcopy(self.task) + + while True: + # get the message (number of steps, parameters, etc). + msg = self.__recv(rank=self.process_id) + + # end the process if specified, i.e. if the number of steps is negative + if msg[0] == -1: + break + + # decompose the received message + num_steps, render, verbose, parameters = msg + + # set the parameters of the policy + self.explorer.policy.copy_parameters(parameters) + + # perform a rollout + trajectory = self.rollout(num_steps=num_steps, deterministic=False, render=render, verbose=verbose) + + # return the process id and the trajectory + self.__send((self.process_id, trajectory), dst=0, rank=self.process_id) + + def _explore(self, num_steps, num_rollouts=1, deterministic=False, render=False, verbose=False): + """ + Explore in the environment. + + Args: + num_steps (int): number of steps in one episode in the on-policy case. This is also the number of updates + in the off-policy case. + num_rollouts (int): number of trajectories/rollouts (only valid in the on-policy case). + deterministic (bool): if deterministic is True, then it does not explore in the environment. + render (bool): if we should render the environment. + verbose (bool): If true, print information on the standard output. + + Returns: + DictStorage: updated memory storage + """ + for rollout in range(num_rollouts): + # reset environment + observation = self.env.reset() + + if verbose: + print("Start rollout: {}/{}".format(rollout + 1, num_rollouts)) + # print("Explorer - initial state: {}".format(observation)) + + # reset storage + self.storage.reset(init_states=observation, rollout_idx=rollout) + + # reset explorer + self.explorer.reset() + + # run RL task for T steps + for step in range(num_steps): + # if we need to render + if render: + self.env.render() + + # get action and corresponding distribution from policy + action, distribution = self.explorer.act(observation, deterministic=deterministic) + + # perform one step in the environment + next_observation, reward, done, info = self.env.step(action) + + # if verbose: + # print("\nExplorer:") + # print("1. Observation data: {}".format(observation)) + # print("2. Action data: {}".format(action)) + # print("3. Next observation data: {}".format(next_observation)) + # print("4. Reward: {}".format(reward)) + # print("5. \\pi(.|s): {}".format(distribution)) + # print("6. log \\pi(a|s): {}".format([d.log_prob(action) for d in distribution])) + + # insert in storage + self.storage.insert(observation, action, next_observation, reward, mask=(1 - done), + distributions=distribution, rollout_idx=rollout) + + # set current observation to current one + observation = next_observation + + # if done, get out of the loop + if done: + break + + # fill remaining mask values + self.storage.end(rollout) + + if verbose: + print("End rollout: {}/{}".format(rollout + 1, num_rollouts)) + # print("states: {}".format(self.storage['states'])) + # print("actions: {}".format(self.storage['actions'])) + # print("rewards: {}".format(self.storage['rewards'])) + # print("masks: {}".format(self.storage['masks'])) + # print("distributions: {}".format(self.storage['distributions'])) + + # # clear explorer + # self.explorer.clear() # return storage unit return self.storage + def explore(self, num_steps, num_rollouts=1, deterministic=False, render=False, verbose=False): + """ + Explore in the environment. + + Args: + num_steps (int): number of steps in one episode in the on-policy case. This is also the number of updates + in the off-policy case. + num_rollouts (int): number of trajectories/rollouts (only valid in the on-policy case). + deterministic (bool): if deterministic is True, then it does not explore in the environment. + render (bool): if we should render the environment. + verbose (bool): If true, print information on the standard output. + + Returns: + RolloutStorage, ExperienceReplay: updated memory storage + """ + if self.num_workers > 1: # parallel exploration + return self._master_explore(num_steps, num_rollouts=num_rollouts, render=render, verbose=verbose) + else: # simple exploration + return self._explore(num_steps, num_rollouts=num_rollouts, deterministic=deterministic, render=render, + verbose=verbose) + ############# # Operators # ############# + def __del__(self): + """Delete the exploration phase; this will close the processes.""" + self.close() + def __repr__(self): """Return a representation string about the class.""" return self.__class__.__name__ @@ -204,16 +628,17 @@ class Explorer(object): """Return a string describing the class.""" return self.__class__.__name__ - def __call__(self, num_steps, rollout_idx=0, deterministic=False, verbose=True): + def __call__(self, num_steps, num_rollouts=1, deterministic=False, verbose=True): # rollout_idx """Explore in the environment. Args: - num_steps (int): number of steps - rollout_idx (int): trajectory/rollout index. + num_steps (int): number of steps in one episode in the on-policy case. This is also the number of updates + in the off-policy case. + num_rollouts (int): number of trajectories/rollouts (only valid in the on-policy case). deterministic (bool): if deterministic is True, then it does not explore in the environment. verbose (bool): If true, print information on the standard output. Returns: DictStorage: updated memory storage """ - self.explore(num_steps, rollout_idx=rollout_idx) + self.explore(num_steps, num_rollouts=num_rollouts, deterministic=deterministic, verbose=verbose) diff --git a/pyrobolearn/algos/rl_algo.py b/pyrobolearn/algos/rl_algo.py index 2fbb06d..f4f37a8 100755 --- a/pyrobolearn/algos/rl_algo.py +++ b/pyrobolearn/algos/rl_algo.py @@ -8,7 +8,6 @@ Dependencies: """ import numpy as np -# from pathos.multiprocessing import Pool from pyrobolearn.storages import RolloutStorage @@ -149,7 +148,7 @@ class RLAlgo(object): # Algo): [5] OpenAI - Spinning Up: https://spinningup.openai.com/ """ - def __init__(self, explorer, evaluator, updater, dynamic_model=None): # , hyperparameters={}, num_workers=1): + def __init__(self, explorer, evaluator, updater, dynamic_model=None): """ Initialize the reinforcement learning algorithm. @@ -327,19 +326,22 @@ class RLAlgo(object): # Algo): # for each episode for episode in range(num_episodes): - # for each rollout - for rollout in range(num_rollouts): - # TODO: consider to learn the dynamic model if provided + # # for each rollout + # for rollout in range(num_rollouts): + # # TODO: consider to learn the dynamic model if provided + # + # if verbose: + # print("Episode: {}/{} - Rollout: {}/{}".format(episode+1, num_episodes, rollout+1, num_rollouts)) + # + # # Explore + # self.explorer.explore(num_steps, rollout, verbose=verbose) - if verbose: - print("Episode: {}/{} - Rollout: {}/{}".format(episode+1, num_episodes, rollout+1, num_rollouts)) - - # Explore - self.explorer.explore(num_steps, rollout, verbose=verbose) - - # evaluate and update + # 1. explore + self.explorer.explore(num_steps, num_rollouts, verbose=verbose) + # 2. evaluate if self.evaluator is not None: self.evaluator.evaluate(verbose=verbose) + # 3. update losses = self.updater.update(verbose=verbose) # add the loss in the history diff --git a/pyrobolearn/approximators/approximator.py b/pyrobolearn/approximators/approximator.py index 9311358..ed241cc 100644 --- a/pyrobolearn/approximators/approximator.py +++ b/pyrobolearn/approximators/approximator.py @@ -339,6 +339,18 @@ class Approximator(object): """Set the inner model into testing mode.""" self.model.eval() + def copy_parameters(self, parameters): + """Copy the given parameters. + + Args: + parameters (Approximator, Model, torch.nn.Module, generator, iterable): the other policy's parameters to + copy. + """ + if isinstance(parameters, (self.__class__, self.model.__class__)): + self.model.copy_parameters(parameters.parameters()) + else: + self.model.copy_parameters(parameters) + def parameters(self): """Return an iterator over the approximator parameters.""" return self.model.parameters() diff --git a/pyrobolearn/envs/env.py b/pyrobolearn/envs/env.py index d34cc73..3021c22 100644 --- a/pyrobolearn/envs/env.py +++ b/pyrobolearn/envs/env.py @@ -88,7 +88,9 @@ class Env(object): # gym.Env): # TODO: make it inheriting the gym.Env self.extra_info = extra_info if extra_info is not None else lambda: False self.actions = actions - self.rendering = False # check with simulator + # check if we are rendering with the simulator + self.is_rendering = self.simulator.is_rendering() + self.rendering_mode = 'human' # save the world state in memory self.initial_world_state = self.world.save() @@ -337,8 +339,7 @@ class Env(object): # gym.Env): # TODO: make it inheriting the gym.Env # compute reward # rewards = [reward.compute() for reward in self.rewards] - if self.rewards is not None: - rewards = self.rewards() + rewards = self.rewards() if self.rewards is not None else None # compute terminating condition done = any([condition() for condition in self.terminal_conditions]) @@ -355,10 +356,13 @@ class Env(object): # gym.Env): # TODO: make it inheriting the gym.Env def render(self, mode='human'): """Renders the environment (show the GUI).""" + self.is_rendering = True + self.rendering_mode = mode self.sim.render() def hide(self): """hide the GUI.""" + self.is_rendering = False self.sim.hide() def close(self): diff --git a/pyrobolearn/envs/gym_wrapper.py b/pyrobolearn/envs/gym_wrapper.py index 371b3de..10b3174 100644 --- a/pyrobolearn/envs/gym_wrapper.py +++ b/pyrobolearn/envs/gym_wrapper.py @@ -101,6 +101,9 @@ class GymEnvWrapper(gym.Env): self.state_processors = state_processors self.reward_processors = reward_processors + # rendering + self.is_rendering = False + ############## # Properties # ############## @@ -270,11 +273,12 @@ class GymEnvWrapper(gym.Env): def render(self, mode='human'): """Render the gym environment.""" + self.is_rendering = True self.env.render(mode) def hide(self): """Hide the gym environment (not used).""" - pass + self.is_rendering = False def close(self): """Close the gym environment.""" diff --git a/pyrobolearn/models/basics/linear.py b/pyrobolearn/models/basics/linear.py index b5459e3..669f241 100644 --- a/pyrobolearn/models/basics/linear.py +++ b/pyrobolearn/models/basics/linear.py @@ -5,6 +5,8 @@ The linear model is a discriminative deterministic model given by: :math:`y = f( """ import copy +import types +import collections try: import cPickle as pickle @@ -162,7 +164,7 @@ class Linear(object): """Copy the given parameters. Args: - parameters (NN, torch.nn.Module, generator, iterable): the other model's parameters to copy. + parameters (Linear, torch.nn.Module, generator, iterable): the other model's parameters to copy. """ if isinstance(parameters, self.__class__): self.model.load_state_dict(parameters.model.state_dict()) @@ -172,8 +174,8 @@ class Linear(object): for model_params, other_params in zip(self.parameters(), parameters): model_params.data.copy_(other_params.data) else: - raise TypeError("Expecting the given parameters to be an instance of `NN`, `torch.nn.Module`, `generator`" - ", or an iterable object, instead got: {}".format(type(parameters))) + raise TypeError("Expecting the given parameters to be an instance of `Linear`, `torch.nn.Module`, " + "`generator`, or an iterable object, instead got: {}".format(type(parameters))) def parameters(self): """Returns an iterator over the model parameters.""" diff --git a/pyrobolearn/models/model.py b/pyrobolearn/models/model.py index fe70669..8c20d81 100644 --- a/pyrobolearn/models/model.py +++ b/pyrobolearn/models/model.py @@ -219,22 +219,27 @@ class Model(object): pass self._models.append(model) - # def copy_parameters(self, parameters): - # """Copy the given parameters. - # - # Args: - # parameters (NN, torch.nn.Module, generator, iterable): the other model's parameters to copy. - # """ - # if isinstance(parameters, self.__class__): - # self.model.load_state_dict(parameters.model.state_dict()) - # elif isinstance(parameters, torch.nn.Module): - # self.model.load_state_dict(parameters.state_dict()) - # elif isinstance(parameters, (types.GeneratorType, collections.Iterable)): - # for model_params, other_params in zip(self.parameters(), parameters): - # model_params.data.copy_(other_params.data) - # else: - # raise TypeError("Expecting the given parameters to be an instance of `NN`, `torch.nn.Module`, `generator`" - # ", or an iterable object, instead got: {}".format(type(parameters))) + def copy_parameters(self, parameters): + """Copy the given parameters. + + Args: + parameters (torch.nn.Module, generator, iterable): the other model's parameters to copy. + """ + if len(self.models) == 1: + self.models[0].copy_parameters(parameters) + else: + for model, parameter in zip(self.models, parameters): + model.copy_parameters(parameter) + # if isinstance(parameters, self.__class__): + # self.model.load_state_dict(parameters.model.state_dict()) + # elif isinstance(parameters, torch.nn.Module): + # self.model.load_state_dict(parameters.state_dict()) + # elif isinstance(parameters, (types.GeneratorType, collections.Iterable)): + # for model_params, other_params in zip(self.parameters(), parameters): + # model_params.data.copy_(other_params.data) + # else: + # raise TypeError("Expecting the given parameters to be an instance of `torch.nn.Module`, `generator`" + # ", or an iterable object, instead got: {}".format(type(parameters))) @abstractmethod def parameters(self): diff --git a/pyrobolearn/policies/policy.py b/pyrobolearn/policies/policy.py index 91277c2..4149f88 100644 --- a/pyrobolearn/policies/policy.py +++ b/pyrobolearn/policies/policy.py @@ -326,6 +326,18 @@ class Policy(object): """ raise self.model.is_recurrent() + def copy_parameters(self, parameters): + """Copy the given parameters. + + Args: + parameters (Policy, Approximator, torch.nn.Module, generator, iterable): the other policy's parameters to + copy. + """ + if isinstance(parameters, (self.__class__, self.model.__class__)): + self.model.copy_parameters(parameters.parameters()) + else: + self.model.copy_parameters(parameters) + def parameters(self): """ Return an iterator over the learning model parameters. diff --git a/pyrobolearn/simulators/README.md b/pyrobolearn/simulators/README.md index 8335857..4a62ad1 100644 --- a/pyrobolearn/simulators/README.md +++ b/pyrobolearn/simulators/README.md @@ -1,13 +1,24 @@ ## Simulators -This folder contains the APIs to the various simulators. Currently, the main simulator being supported is PyBullet. Work is under progress for Gazebo+ROS, and OpenSIM. +This folder contains the APIs to the various simulators. Currently, the main simulator being supported is PyBullet. +Work is under progress for other simulators. ```python import pyrobolearn as prl -sim = prl.simulators.BulletSim() +sim = prl.simulators.Bullet() +sim1 = prl.simulators.Dart() ``` #### What to check next? Check the `worlds` folder and the `robots` folder. + +#### TODOs + +- [x] implement Bullet interface +- [ ] implement Dart interface +- [ ] implement ROS_RBDL interface +- [ ] implement Gazebo_ROS interface +- [ ] implement Mujoco interface +- [ ] implement `simulator_randomizer` (similar to `physics_randomizer`) diff --git a/pyrobolearn/simulators/__init__.py b/pyrobolearn/simulators/__init__.py index e915ed0..1982910 100644 --- a/pyrobolearn/simulators/__init__.py +++ b/pyrobolearn/simulators/__init__.py @@ -5,7 +5,11 @@ from .simulator import Simulator # bullet simulator -from .bullet import Bullet as BulletSim +from .bullet import Bullet +from .bullet import Bullet as BulletSim # alias # TODO: remove that alias + +# dart simulator +from .dart import Dart # # PyBullet simulator # import pybullet diff --git a/pyrobolearn/simulators/bullet.py b/pyrobolearn/simulators/bullet.py index 6421a3d..44da439 100644 --- a/pyrobolearn/simulators/bullet.py +++ b/pyrobolearn/simulators/bullet.py @@ -20,10 +20,9 @@ References: # general imports import os -import inspect +# import inspect import time import numpy as np -import quaternion # import pybullet import pybullet @@ -69,7 +68,7 @@ class Bullet(Simulator): In the following documentation: * `vec3` specifies a list/tuple/np.array of 3 floats - * `quat` specifies a list/tuple/np.quaternion of 4 floats + * `quat` specifies a list/tuple/np.array of 4 floats Examples: sim = Bullet() @@ -800,11 +799,10 @@ class Bullet(Simulator): Args: filename (str): path to file for the mesh. Currently, only Wavefront .obj. It will create convex hulls for each object (marked as 'o') in the .obj file. - position (float[3]): position of the mesh in the Cartesian world space (in meters) - orientation (float[4], np.quaternion): orientation of the mesh using quaternion. - If np.quaternion then it uses the convention (w,x,y,z). If float[4], it uses the convention (x,y,z,w) + position (list of 3 float, np.array[3]): position of the mesh in the Cartesian world space (in meters) + orientation (list of 4 float, np.array[4]): orientation of the mesh using quaternion [x,y,z,w]. mass (float): mass of the mesh (in kg). If mass = 0, it won't move even if there is a collision. - scale (float[3]): scale the mesh in the (x,y,z) directions + scale (list of 3 float, np.array[3]): scale the mesh in the (x,y,z) directions color (int[4], None): color of the mesh for red, green, blue, and alpha, each in range [0,1]. with_collision (bool): If True, it will also create the collision mesh, and not only a visual mesh. flags (int, None): if flag = `sim.GEOM_FORCE_CONCAVE_TRIMESH` (=1), this will create a concave static diff --git a/pyrobolearn/simulators/dart.py b/pyrobolearn/simulators/dart.py index fb6484f..e06625a 100644 --- a/pyrobolearn/simulators/dart.py +++ b/pyrobolearn/simulators/dart.py @@ -293,7 +293,7 @@ class Dart(Simulator): Returns: int (non-negative): unique id associated to the load model. """ - return self.world.add_skeleton(filename) + return self.world.add_skeleton(filename).id def load_sdf(self, filename, scaling=1., *args, **kwargs): """Load a SDF file in the simulator. @@ -305,7 +305,7 @@ class Dart(Simulator): Returns: list(int): list of object unique id for each object loaded """ - return self.world.add_skeleton(filename) + return self.world.add_skeleton(filename).id def load_mjcf(self, filename, scaling=1., *args, **kwargs): """Load a Mujoco file in the simulator. diff --git a/pyrobolearn/tasks/task.py b/pyrobolearn/tasks/task.py index fc470a9..3a2994c 100644 --- a/pyrobolearn/tasks/task.py +++ b/pyrobolearn/tasks/task.py @@ -64,8 +64,19 @@ class Task(object): __metaclass__ = ABCMeta def __init__(self, environment, policies): + """ + Initialize the task. + + Args: + environment (Env, gym.Env): environment. + policies (list of Policy, Policy): the policy(ies). + """ + # check the environment if not isinstance(environment, (Env, gym.Env)): raise TypeError("Expecting 'environment' to be an instance of Env or gym.Env") + self.env = environment + + # check the policies if isinstance(policies, collections.Iterable): for policy in policies: if not isinstance(policy, Policy): @@ -74,9 +85,8 @@ class Task(object): policies = [policies] else: raise TypeError("Expecting 'policies' to be an instance of Policy, or list/tuple of policies") - - self.env = environment self.policies = policies + self._done = False self._succeeded = False @@ -204,6 +214,13 @@ class Task(object): def run(self, num_steps=None, dt=0, use_terminating_condition=False, render=False): """ Reset and run the task until it is done, or the current time step matches num_steps. + + Args: + num_steps (None, int): number of steps to run. + dt (float): time to sleep for the next step in the environment. + use_terminating_condition (bool): if we should continue or not once the terminal condition has been + fulfilled. + render (bool): if we should render the environment or not. """ if num_steps is None: num_steps = np.infty @@ -231,6 +248,10 @@ class Task(object): def step(self, deterministic=True, render=False): """ Perform one step. + + Args: + deterministic (bool): if policy should be deterministic or not. + render (bool): if we should render or not. """ # if we need to render the environment if render: @@ -253,11 +274,29 @@ class Task(object): return np.array(rewards) def get_policy(self, idx=None): + """ + Get the `idx`th policy. + + Args: + idx (int, None): the `idx`th policy to return. If None, it will return the list of policies. + + Returns: + (list of) Policy: policy(ies) + """ if idx is None: return self.policies return self.policies[idx] def get_learning_model(self, idx=None): + """ + Get the learning model associated to the `idx`th policy. + + Args: + idx (int): the `idx`th policy. If None, it will return the list of learning models. + + Returns: + (list of) Model: learning model(s). + """ if idx is None: return [policy.model for policy in self.policies] return self.policies[idx].model @@ -271,9 +310,6 @@ class Task(object): """Load the storage from the disk.""" return pickle.load(open(filename, 'r')) - def rollout(self): # TODO - pass - ############# # Operators # #############