From 5b201702f687be36a58e5177180d5a668e85f491 Mon Sep 17 00:00:00 2001 From: Brian Delhaisse Date: Tue, 3 Sep 2019 06:40:32 +0200 Subject: [PATCH] update simulators --- pyrobolearn/simulators/bullet.py | 14 +- pyrobolearn/simulators/dart.py | 17 +- pyrobolearn/simulators/flex.py | 2 + pyrobolearn/simulators/gazebo.py | 12 +- pyrobolearn/simulators/isaac.py | 330 +++++++++++++++++++++++++++- pyrobolearn/simulators/mujoco.py | 16 +- pyrobolearn/simulators/raisim.py | 18 +- pyrobolearn/simulators/simulator.py | 17 +- 8 files changed, 388 insertions(+), 38 deletions(-) diff --git a/pyrobolearn/simulators/bullet.py b/pyrobolearn/simulators/bullet.py index 07a376d..e3a631e 100644 --- a/pyrobolearn/simulators/bullet.py +++ b/pyrobolearn/simulators/bullet.py @@ -3,7 +3,7 @@ This is the main interface that communicates with the PyBullet simulator [1]. By defining this interface, it allows to decouple the PyRoboLearn framework from the simulator. It also converts some data types to the ones required by -PyBullet. For instance, some methods in PyBullet do not accepts numpy arrays but only lists. The interface provided +PyBullet. For instance, some methods in PyBullet do not accept numpy arrays but only lists. The interface provided here makes the necessary conversions. The signature of each method defined here are inspired by [1,2] but in accordance with the PEP8 style guide [3]. @@ -80,12 +80,13 @@ class Bullet(Simulator): Erwin Coumans and Yunfei Bai, 2017/2018 """ - def __init__(self, render=True, **kwargs): + def __init__(self, render=True, num_instances=1, **kwargs): """ - Initialize PyBullet simulator. + Initialize the PyBullet simulator. Args: render (bool): if True, it will open the GUI, otherwise, it will just run the server. + num_instances (int): number of simulator instances. **kwargs (dict): optional arguments (this is not used here). """ super(Bullet, self).__init__(render=render, **kwargs) @@ -746,9 +747,10 @@ class Bullet(Simulator): Args: filename (str): a relative or absolute path to the URDF file on the file system of the physics server. - position (vec3): create the base of the object at the specified position in world space coordinates [x,y,z] - orientation (quat): create the base of the object at the specified orientation as world space quaternion - [x,y,z,w] + position (np.array[float[3]]): create the base of the object at the specified position in world space + coordinates [x,y,z]. + orientation (np.array[float[4]]): create the base of the object at the specified orientation as world + space quaternion [x,y,z,w]. use_maximal_coordinates (int): Experimental. By default, the joints in the URDF file are created using the reduced coordinate method: the joints are simulated using the Featherstone Articulated Body algorithm (btMultiBody in Bullet 2.x). The useMaximalCoordinates option will create a 6 degree of freedom rigid diff --git a/pyrobolearn/simulators/dart.py b/pyrobolearn/simulators/dart.py index c3a3761..25e79df 100644 --- a/pyrobolearn/simulators/dart.py +++ b/pyrobolearn/simulators/dart.py @@ -95,7 +95,15 @@ class Dart(Simulator): - [2] PEP8: https://www.python.org/dev/peps/pep-0008/ """ - def __init__(self, render=True, dt=0.001, **kwargs): + def __init__(self, render=True, num_instances=1, dt=0.001, **kwargs): + """ + Initialize the Dart simulator. + + Args: + render (bool): if True, it will open the GUI, otherwise, it will just run the server. + num_instances (int): number of simulator instances. + **kwargs (dict): optional arguments (this is not used here). + """ super(Dart, self).__init__(render, **kwargs) # dart = {'collision': ['BulletCollisionDetector', 'BulletCollisionGroup', 'CollisionDetector', @@ -521,9 +529,10 @@ class Dart(Simulator): Args: filename (str): a relative or absolute path to the URDF file on the file system of the physics server. - position (vec3): create the base of the object at the specified position in world space coordinates [x,y,z] - orientation (quat): create the base of the object at the specified orientation as world space quaternion - [x,y,z,w] + position (np.array[float[3]]): create the base of the object at the specified position in world space + coordinates [x,y,z]. + orientation (np.array[float[4]]): create the base of the object at the specified orientation as world + space quaternion [x,y,z,w]. use_fixed_base (bool): force the base of the loaded object to be static scale (float): scale factor to the URDF model. diff --git a/pyrobolearn/simulators/flex.py b/pyrobolearn/simulators/flex.py index c99b12a..8184df6 100644 --- a/pyrobolearn/simulators/flex.py +++ b/pyrobolearn/simulators/flex.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """Define the Nvidia FleX Simulator API. +DEPRECATED: See `isaac.py`. + This is the main interface that communicates with the FleX simulator [1]. By defining this interface, it allows to decouple the PyRoboLearn framework from the simulator. It also converts some data types to the ones required by FleX. diff --git a/pyrobolearn/simulators/gazebo.py b/pyrobolearn/simulators/gazebo.py index 7c69e43..6e19986 100644 --- a/pyrobolearn/simulators/gazebo.py +++ b/pyrobolearn/simulators/gazebo.py @@ -15,7 +15,7 @@ References: [1] Gazebo: http://gazebosim.org/ """ -# TODO +# TODO: create Gazebo wrapper or use ROS to communicate... from pyrobolearn.simulators.simulator import Simulator @@ -36,6 +36,14 @@ class Gazebo(Simulator): [1] Gazebo: http://gazebosim.org/ """ - def __init__(self, render=True): + def __init__(self, render=True, num_instances=1, **kwargs): + r""" + Initialize the Gazebo Simulator. + + Args: + render (bool): if True, it will open the GUI, otherwise, it will just run the server. + num_instances (int): number of simulator instances. + **kwargs (dict): optional arguments (this is not used here). + """ super(Gazebo, self).__init__(render=render) raise NotImplementedError diff --git a/pyrobolearn/simulators/isaac.py b/pyrobolearn/simulators/isaac.py index 418be22..8a44d72 100644 --- a/pyrobolearn/simulators/isaac.py +++ b/pyrobolearn/simulators/isaac.py @@ -1,8 +1,10 @@ #!/usr/bin/env python """Define the Isaac SDK simulator API. -This is the main interface that communicates with the Isaac SDK simulator [1]. By defining this interface, it allows to -decouple the PyRoboLearn framework from the simulator. +This is the main interface that communicates with the Isaac SDK/Gym simulator [1]. By defining this interface, it +allows to decouple the PyRoboLearn framework from the simulator. It also converts some data types to the ones required +by Isaac Gym. For instance, some methods in `isaacgym` do not accept numpy arrays but only `gymapi` data types such +as `gymapi.Vec3`, `gymapi.Quat`, and others. The interface provided here makes the necessary conversions. The signature of each method defined here are inspired by [2] but in accordance with the PEP8 style guide [3]. Parts of the documentation for the methods have been copied-pasted from [2] for completeness purposes. @@ -11,15 +13,25 @@ Dependencies in PRL: * `pyrobolearn.simulators.simulator.Simulator` References: - [1] Isaac SDK: https://developer.nvidia.com/isaac-sdk - [2] PyBullet Quickstart Guide: https://docs.google.com/document/d/10sXEhzFRSnvFcl3XxNGhnD4N2SedqwdAvK3dsihxVUA - [3] PEP8: https://www.python.org/dev/peps/pep-0008/ + - [1] Isaac SDK: https://developer.nvidia.com/isaac-sdk + - [2] PyBullet Quickstart Guide: https://docs.google.com/document/d/10sXEhzFRSnvFcl3XxNGhnD4N2SedqwdAvK3dsihxVUA + - [3] PEP8: https://www.python.org/dev/peps/pep-0008/ + - [4] Isaac gym slides: + https://developer.download.nvidia.com/video/gputechconf/gtc/2019/presentation/s9918-isaac-gym.pdf """ -# TODO: waiting for its release at the end of March +# TODO: waiting for its release in September +import os import time import numpy as np +from collections import OrderedDict + +try: + import isaacgym + from isaacgym import gymapi +except ImportError as e: + raise ImportError(str(e) + "\nTry to install `Isaac Gym`!") from pyrobolearn.simulators.simulator import Simulator @@ -43,11 +55,307 @@ class Isaac(Simulator): operate and cooperate with humans." [1] References: - [1] https://developer.nvidia.com/isaac-sdk - [2] https://www.nvidia.com/en-au/deep-learning-ai/industries/robotics/ - [3] "GPU-Accelerated Robotic Simulation for Distributed Reinforcement Learning", Liang et al., 2018 + - [1] https://developer.nvidia.com/isaac-sdk + - [2] https://www.nvidia.com/en-au/deep-learning-ai/industries/robotics/ + - [3] "GPU-Accelerated Robotic Simulation for Distributed Reinforcement Learning", Liang et al., 2018 + - [4] Slides: https://developer.download.nvidia.com/video/gputechconf/gtc/2019/presentation/s9918-isaac-gym.pdf """ - def __init__(self, render=True, **kwargs): + def __init__(self, render=True, num_instances=1, **kwargs): + """ + Initialize Isaac gym simulator. + + Args: + render (bool): if True, it will open the GUI, otherwise, it will just run the server. + num_instances (int): number of simulator instances. + **kwargs (dict): optional arguments (this is not used here). + """ super(Isaac, self).__init__(render=render) - raise NotImplementedError + + # define variables + self.gym = gymapi.acquire_gym() + self.sim = self.gym.create_sim() + self.params = gymapi.SimParams() + self.gym.get_sim_params(self.sim, self.params) + spacing = 10 + lower, upper = gymapi.Vec3(-spacing, -spacing, 0.), gymapi.Vec3(spacing, spacing, spacing) + self.env = self.gym.create_env(self.sim, lower, upper) + self.viewer = None + + self.dt = 1./60 + self.num_substeps = 2 + + if render: # gui + self.viewer = self.gym.create_viewer(None, 1920, 1080) + + # keep track of loaded bodies + self.bodies = OrderedDict() # {body_id: Body} + self._body_cnt = 0 + + ################## + # Static methods # + ################## + + @staticmethod + def simulate_gas_dynamics(): + """Return True if the simulator can simulate gases.""" + return False + + @staticmethod + def simulate_liquid_dynamics(): + """Return True if the simulator can simulate liquids.""" + return True # using Flex + + @staticmethod + def simulate_fluid_dynamics(): + """Return True if the simulator can simulate fluids (gases and liquids).""" + return Simulator.simulate_gas_dynamics() and Simulator.simulate_liquid_dynamics() + + @staticmethod + def simulate_soft_bodies(): + """Return True if the simulator can simulate soft bodies.""" + return True # using Flex + + @staticmethod + def has_middleware_communication_layer(): + """Return True if the simulator has a middleware communication layer (like ROS, YARP, etc).""" + return False + + @staticmethod + def supports_dynamic_loading(): + """Return True if the simulator supports the dynamic loading of models.""" + return True + + @staticmethod + def supports_acceleration(): + """Return True if the simulator provides acceleration (dynamic) information (such as joint accelerations, link + Cartesian accelerations, etc). If not, the `Robot` class will have to implement these using finite + difference.""" + return False + + @staticmethod + def supports_sensors(sensor_type=None): + """Return True if the simulator provides supports for the specified sensor.""" + return False + + @staticmethod + def supports_urdf(): + """Return True if we can use URDFs.""" + return True + + @staticmethod + def supports_light(): + """Return True if we can define and access to the lights in the simulator.""" + return False + + @staticmethod + def supports_depth_image(): + """Return True if we can get depth images from the simulator.""" + return False + + @staticmethod + def supports_segmentation_images(): + """Return True if we can get segmentation images from the simulator.""" + return False + + @staticmethod + def supports_visualization(): + """Return True if there is a graphical user interface (GUI).""" + return True + + @staticmethod + def supports_interactive_gui(): + """Return True if the simulator has an interactive GUI.""" + return False + + @staticmethod + def supports_mousekeyboard_events(): + """Return True if the simulator allows to capture mouse and keyboard events.""" + return False + + @staticmethod + def supports_visual_objects(): + """Return True if we can simulate objects that do not have collision shapes.""" + return False + + @staticmethod + def supports_plugins(): + """Return True if we can use plugins.""" + return False + + @staticmethod + def supports_constraints(constraint_type): + """Return True if we can support the specified constraint type.""" + return False + + @staticmethod + def supports_realtime(): + """Return True if the simulator supports real-time (meaning we don't need to step manually in the simulator). + Note that if we can step in the simulator, we can use threads to simulate the real-time. So the return value + should always be True.""" + return True + + @staticmethod + def supports_ray_casting(): + """Return True if the simulator supports ray casting.""" + return False + + @staticmethod + def can_step(): + """Return True if we can step manually in the simulator.""" + return True + + @staticmethod + def can_load_heightmap(): + """Return True if the simulator can load a heightmap.""" + return False + + ########### + # Methods # + ########### + + ############## + # Simulators # + ############## + + def step(self, sleep_time=0): + """Perform a step in the simulator, and sleep the specified amount of time. + + Args: + sleep_time (float): amount of time to sleep after performing one step in the simulation. + """ + # step the simulation + self.gym.simulate(self.sim, self.dt, self.num_substeps) + self.gym.fetch_results(self.sim, True) + + # update the viewer + if self.viewer is not None: + if self.gym.query_viewer_has_closed(self.viewer): + exit() + self.gym.step_graphics(self.sim) + self.gym.draw_viewer(self.viewer, self.sim, True) + + # wait for dt to elapse in real-time. + # This synchronizes the physics simulation with the rendering rate. + self.gym.sync_frame_time(self.sim) + + # time.sleep(sleep_time) + + def render(self, enable=True): + """Render the simulation. + + Args: + enable (bool): If True, it will render the simulator by enabling the GUI. + """ + self._render = enable + if self._render: + if self.viewer is None: + self.viewer = self.gym.create_viewer(None, 1920, 1080) + else: + if self.viewer is not None: + pass # close the viewer set viewer to None + + def get_physics_properties(self): + """Get the physics engine parameters. + + Returns: + dict: dictionary containing the physics simulator properties. + """ + properties = dict() + gravity = self.params.gravity # return gymapi.Vec3 + gravity = np.array([gravity[0], gravity[1], gravity[2]]) + properties['gravity'] = gravity + properties['solver_type'] = self.params.solver_type + properties['num_outer_iterations'] = self.params.num_outer_iterations + properties['num_inner_iterations'] = self.params.num_inner_iterations + properties['relaxation'] = self.params.relaxation + properties['warm_start'] = self.params.warm_start + properties['num_substeps'] = self.num_substeps + return properties + + def set_physics_properties(self, solver_type=None, num_outer_iterations=None, num_inner_iterations=None, + relaxation=None, warm_start=None, num_substeps=None, *args, **kwargs): + """Set the physics engine parameters.""" + if solver_type is not None: + self.params.solver_type = int(solver_type) + if num_outer_iterations is not None: + self.params.num_outer_iterations = int(num_outer_iterations) + if num_inner_iterations is not None: + self.params.num_inner_iterations = int(num_inner_iterations) + if relaxation is not None: + self.params.relaxation = float(relaxation) + if warm_start is not None: + self.params.warm_start = float(warm_start) + if num_substeps is not None: + self.num_substeps = int(num_substeps) if num_substeps >= 1 else 1 + + def get_gravity(self): + """Return the gravity set in the simulator.""" + gravity = self.params.gravity # return gymapi.Vec3 + return np.asarray([gravity[0], gravity[1], gravity[2]]) + + def set_gravity(self, gravity=(0, 0, -9.81)): + """Set the gravity in the simulator with the given acceleration. + + By default, there is no gravitational force enabled in the simulator. + + Args: + gravity (list, tuple of 3 floats): acceleration in the x, y, z directions. + """ + gravity = gymapi.Vec3(*gravity) + self.params.gravity = gravity + self.gym.set_sim_params(self.sim, self.params) + + ###################################### + # Loading URDFs, SDFs, MJCFs, meshes # + ###################################### + + def load_urdf(self, filename, position, orientation, use_fixed_base=0, scale=1.0, *args, **kwargs): + """Load a URDF file in the simulator. + + Args: + filename (str): a relative or absolute path to the URDF file on the file system of the physics server. + position (np.array[float[3]]): create the base of the object at the specified position in world space + coordinates [x,y,z]. + orientation (np.array[float[4]]): create the base of the object at the specified orientation as world + space quaternion [x,y,z,w]. + use_fixed_base (bool): force the base of the loaded object to be static + scale (float): scale factor to the URDF model. + + Returns: + int (non-negative): unique id associated to the load model. + """ + dirname = os.path.dirname(filename) + filename = os.path.basename(filename) + name = ''.join(filename.split('.')[:-1]) + asset = self.gym.load_asset(dirname, filename) + + position = gymapi.Vec3(*position) + orientation = gymapi.Quat(*orientation) + pose = gymapi.Transform(position, orientation) + self.gym.create_actor(self.env, asset, pose, name) + + self._body_cnt += 1 + self.bodies[self._body_cnt] = name + return self._body_cnt + + def load_mjcf(self, filename, scaling=1., *args, **kwargs): + """Load a Mujoco file in the simulator. + + Args: + filename (str): a relative or absolute path to the MJCF file on the file system of the physics server. + scaling (float): scale factor for the object + + Returns: + list(int): list of object unique id for each object loaded + """ + dirname = os.path.dirname(filename) + filename = os.path.basename(filename) + name = ''.join(filename.split('.')[:-1]) + asset = self.gym.load_asset(dirname, filename) + + self.gym.create_actor(self.env, asset, name) + + self._body_cnt += 1 + self.bodies[self._body_cnt] = name + return self._body_cnt diff --git a/pyrobolearn/simulators/mujoco.py b/pyrobolearn/simulators/mujoco.py index 83fc71b..73d544b 100644 --- a/pyrobolearn/simulators/mujoco.py +++ b/pyrobolearn/simulators/mujoco.py @@ -209,16 +209,17 @@ class Mujoco(Simulator): - [3] DeepMind Control Suite: https://github.com/deepmind/dm_control/tree/master/dm_control/mujoco """ - def __init__(self, render=True, load_at_the_end=False): + def __init__(self, render=True, num_instances=1, load_at_the_end=False): """ Initialize the MuJoCo simulator. Args: render (bool): if True, it will open the GUI, otherwise, it will just run the server (i.e. in a headless - mode, i.e. without a GUI). + mode, i.e. without a GUI). + num_instances (int): number of simulator instances. load_at_the_end (bool): if True, it will load at the end all the models that have been "loaded" in the - simulator. The reason is that the MuJoCo simulator does not allow to load dynamically models into the - world. + simulator. The reason is that the MuJoCo simulator does not allow to load dynamically models into the + world. """ super(Mujoco, self).__init__(render=render) @@ -562,9 +563,10 @@ class Mujoco(Simulator): Args: filename (str): a relative or absolute path to the URDF file on the file system of the physics server. - position (vec3): create the base of the object at the specified position in world space coordinates [x,y,z] - orientation (quat): create the base of the object at the specified orientation as world space quaternion - [x,y,z,w] + position (np.array[float[3]]): create the base of the object at the specified position in world space + coordinates [x,y,z]. + orientation (np.array[float[4]]): create the base of the object at the specified orientation as world + space quaternion [x,y,z,w]. use_fixed_base (bool): force the base of the loaded object to be static scale (float): scale factor to the URDF model. diff --git a/pyrobolearn/simulators/raisim.py b/pyrobolearn/simulators/raisim.py index 0446fd7..6a59463 100644 --- a/pyrobolearn/simulators/raisim.py +++ b/pyrobolearn/simulators/raisim.py @@ -41,6 +41,7 @@ except ImportError as e: from pyrobolearn.simulators.simulator import Simulator from pyrobolearn.utils.decorator import keyboard_interrupt + __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2019, PyRoboLearn" __credits__ = ["RaiSim (ETHz, Hwangbo, Kang, Lee)", "Brian Delhaisse (raisimpy + PRL)"] @@ -68,7 +69,15 @@ class Raisim(Simulator): - [6] RaiSimPy - A Python wrapper for Raisim: https://github.com/robotlearn/raisimpy """ - def __init__(self, render=True, **kwargs): + def __init__(self, render=True, num_instances=1, **kwargs): + """ + Initialize the Raisim simulator. + + Args: + render (bool): if True, it will open the GUI, otherwise, it will just run the server. + num_instances (int): number of simulator instances. + **kwargs (dict): optional arguments (this is not used here). + """ super(Raisim, self).__init__(render, **kwargs) # create world @@ -487,9 +496,10 @@ class Raisim(Simulator): Args: filename (str): a relative or absolute path to the URDF file on the file system of the physics server. - position (vec3): create the base of the object at the specified position in world space coordinates [x,y,z] - orientation (quat): create the base of the object at the specified orientation as world space quaternion - [x,y,z,w] + position (np.array[float[3]]): create the base of the object at the specified position in world space + coordinates [x,y,z]. + orientation (np.array[float[4]]): create the base of the object at the specified orientation as world + space quaternion [x,y,z,w]. use_fixed_base (bool): force the base of the loaded object to be static scale (float): scale factor to the URDF model. diff --git a/pyrobolearn/simulators/simulator.py b/pyrobolearn/simulators/simulator.py index 6b5226e..2529f5c 100644 --- a/pyrobolearn/simulators/simulator.py +++ b/pyrobolearn/simulators/simulator.py @@ -182,7 +182,15 @@ class Simulator(object): URDF_USE_SELF_COLLISION_EXCLUDE_PARENT = 16 URDF_USE_SELF_COLLISION_INCLUDE_PARENT = 8192 - def __init__(self, render=True, **kwargs): + def __init__(self, render=True, num_instances=1, **kwargs): + r""" + Initialize the Simulator. + + Args: + render (bool): if True, it will open the GUI, otherwise, it will just run the server. + num_instances (int): number of simulator instances. + **kwargs (dict): optional arguments (this is not used here). + """ self._render = render self.real_time = False self.kwargs = kwargs @@ -560,9 +568,10 @@ class Simulator(object): Args: filename (str): a relative or absolute path to the URDF file on the file system of the physics server. - position (vec3): create the base of the object at the specified position in world space coordinates [x,y,z] - orientation (quat): create the base of the object at the specified orientation as world space quaternion - [x,y,z,w] + position (np.array[float[3]]): create the base of the object at the specified position in world space + coordinates [x,y,z]. + orientation (np.array[float[4]]): create the base of the object at the specified orientation as world + space quaternion [x,y,z,w]. use_fixed_base (bool): force the base of the loaded object to be static scale (float): scale factor to the URDF model.