From bcfe7538bf3bfdaefba9a752821fc606befb1f20 Mon Sep 17 00:00:00 2001 From: Brian Delhaisse Date: Fri, 19 Jul 2019 03:32:11 +0200 Subject: [PATCH] add 2 control envs + update terminal conditions --- pyrobolearn/__init__.py | 5 +- pyrobolearn/actions/robot_actions/__init__.py | 2 +- .../actions/robot_actions/joint_actions.py | 191 +++++++--- .../actions/robot_actions/link_actions.py | 21 +- pyrobolearn/control/README.md | 2 + pyrobolearn/controllers/README.md | 2 + pyrobolearn/envs/control/acrobot.py | 137 +++++++ pyrobolearn/envs/control/cartpole.py | 192 ++++++++++ pyrobolearn/envs/env.py | 4 +- .../physics/joint_physics_randomizer.py | 4 +- .../physics/link_physics_randomizer.py | 4 +- pyrobolearn/priorities/README.rst | 2 + pyrobolearn/robots/acrobot.py | 2 + pyrobolearn/robots/actuators/actuator.py | 11 +- pyrobolearn/robots/cartpole.py | 2 - pyrobolearn/robots/pendulum.py | 2 + pyrobolearn/robots/robot.py | 32 +- pyrobolearn/robots/sensors/sensor.py | 6 +- .../states/generators/state_generator.py | 7 +- pyrobolearn/terminal_conditions/__init__.py | 18 +- .../terminal_conditions/basic_conditions.py | 146 ++++++++ .../terminal_conditions/body_conditions.py | 340 ++++++++++++++++++ .../terminal_conditions/joint_conditions.py | 299 +++++++++++++++ .../terminal_conditions/link_conditions.py | 230 ++++++++++++ .../terminal_conditions/robot_condition.py | 59 +++ .../terminal_conditions/terminal_condition.py | 147 ++++++-- 26 files changed, 1755 insertions(+), 112 deletions(-) create mode 100644 pyrobolearn/envs/control/acrobot.py create mode 100644 pyrobolearn/envs/control/cartpole.py create mode 100644 pyrobolearn/terminal_conditions/basic_conditions.py create mode 100644 pyrobolearn/terminal_conditions/body_conditions.py create mode 100644 pyrobolearn/terminal_conditions/joint_conditions.py create mode 100644 pyrobolearn/terminal_conditions/link_conditions.py create mode 100644 pyrobolearn/terminal_conditions/robot_condition.py diff --git a/pyrobolearn/__init__.py b/pyrobolearn/__init__.py index d40ca80..4388627 100644 --- a/pyrobolearn/__init__.py +++ b/pyrobolearn/__init__.py @@ -36,6 +36,9 @@ from . import states # import actions from . import actions +# import terminal conditions +from . import terminal_conditions + # import rewards from . import rewards @@ -91,7 +94,7 @@ from . import algos __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "MIT" +__license__ = "GNU GPLv3" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/actions/robot_actions/__init__.py b/pyrobolearn/actions/robot_actions/__init__.py index 087116d..dc31d8d 100644 --- a/pyrobolearn/actions/robot_actions/__init__.py +++ b/pyrobolearn/actions/robot_actions/__init__.py @@ -6,7 +6,7 @@ from .robot_actions import RobotAction from .joint_actions import JointAction, JointPositionAction, JointPositionChangeAction, JointVelocityAction, \ JointVelocityChangeAction, JointPositionAndVelocityAction, JointPositionAndVelocityChangeAction, \ JointTorqueAction, JointForceAction, JointTorqueGravityCompensationAction, JointTorqueChangeAction, \ - JointAccelerationAction + JointAccelerationAction, JointAccelerationChangeAction # import the link / end-effector actions from .link_actions import LinkAction, LinkPositionAction, LinkPositionChangeAction, LinkOrientationAction, \ diff --git a/pyrobolearn/actions/robot_actions/joint_actions.py b/pyrobolearn/actions/robot_actions/joint_actions.py index cb4aec9..3565b91 100644 --- a/pyrobolearn/actions/robot_actions/joint_actions.py +++ b/pyrobolearn/actions/robot_actions/joint_actions.py @@ -71,10 +71,11 @@ class JointAction(RobotAction): if not isinstance(value, (list, tuple, np.ndarray)): raise TypeError("Expecting each discrete value set to be a list/tuple/np.ndarray, instead got: " "{} at index {}".format(type(value), i)) - discrete_values[i] = np.asarray(value) - if len(discrete_values.shape) != 1: + value = np.asarray(value) + discrete_values[i] = value + if value.ndim != 1: raise ValueError("Expecting each discrete value set to be a 1D array, instead got a shape of: " - "{}".format(discrete_values.shape)) + "{}".format(value.shape)) # set the discrete values self.discrete_values = discrete_values @@ -84,14 +85,15 @@ class JointAction(RobotAction): # set the space if len(self.discrete_values) == 1: self._space = gym.spaces.Discrete(len(self.discrete_values)) + self.discrete_values = self.discrete_values[0] else: self._space = gym.spaces.MultiDiscrete([len(value) for value in self.discrete_values]) # set the data if isinstance(self._space, gym.spaces.Discrete): - self.data = 0 + self.data = np.zeros(1, dtype=np.int) # the first index is the default values else: - self.data = np.zeros(len(self._space.nvec)) + self.data = np.zeros(len(self._space.nvec), dtype=np.int) # the first indices are the default values # @property # def size(self): @@ -99,12 +101,17 @@ class JointAction(RobotAction): def _check_continuous_bounds(self, bounds): """Check the given continuous bounds.""" + # check the type of the bounds if not isinstance(bounds, (tuple, list, np.ndarray)): raise TypeError("Expecting the given bounds to be a tuple/list/np.ndarray of float, instead got: " "{}".format(type(bounds))) + + # check that the bounds have a length of 2 (i.e. lower and upper bounds) if len(bounds) != 2: raise ValueError("Expecting the bounds to be of length 2 (i.e. lower and upper bounds), instead got a " "length of {}".format(len(bounds))) + + # if both bounds are not None, reshape if necessary if bounds[0] is not None and bounds[1] is not None: bounds = np.asarray(bounds).reshape(2, -1) if len(self.joints) != bounds.shape[1]: @@ -117,28 +124,70 @@ class JointAction(RobotAction): bounds = tuple(bounds) return bounds - # def _write(self, data): - # """ - # Write the data. - # - # Args: - # data (int, np.ndarray): the data can be discrete or continuous. - # """ - # # if the action is discrete, then the data should be an index, or an array of values from which takes the max - # if self.is_discrete(): - # if isinstance(data, np.ndarray): - # data = np.argmax(data.reshape(-1)) - # data = self.discrete_values[data] - # self._write_continuous(data) - # - # def _write_continuous(self, data): - # """ - # Write the given continuous data. Child method that has to be implement in the child classes. - # - # Args: - # data (np.ndarray): continuous data to be written. - # """ - # raise NotImplementedError + def _write(self, data): + """ + Write the data. + + Args: + data (int, np.ndarray): the data can be discrete or continuous. + """ + # - if the action is discrete, then the data should be an index, or an array of values from which takes the max + # - if the action is multi-discrete, then the data should be an array of index, or a list of array values from + # which to take the max for each array + if self.discrete_values is not None: + # if the action is discrete + if isinstance(self._space, gym.spaces.Discrete): + # if the data is an array of values, take the argmax + if isinstance(data, np.ndarray): + if data.size > 1: + data = np.argmax(data.reshape(-1)) + else: + data = int(data.reshape(-1)[0]) + + # elif not an index, raise an error + elif not isinstance(data, int): + raise TypeError("Expecting the given data to be an int (representing the index) for the discrete " + "values, instead got: {}".format(type(data))) + + # take the corresponding "continuous" data + data = self.discrete_values[data] + + # if action is multi-discrete + elif isinstance(self._space, gym.spaces.MultiDiscrete): + + # check the type of the data + if isinstance(data, (list, np.ndarray)): + raise TypeError("Expecting the data to be an list of int/np.array, or a np.array, instead got: " + "{}".format(type(data))) + + # check that the number of rows match the number of discrete actions + if len(data) != self._space.shape[0]: + raise ValueError("The given data does not have the same length (={}) as the number of discrete " + "action (={})".format(len(data), self._space.shape[0])) + + # check each data row and convert it to index if necessary + data_tmp = [] + for d in data: + if isinstance(d, np.ndarray): + d = np.argmax(d.reshape(-1)) + elif not isinstance(d, int): + raise TypeError("Expecting the given data to be an int (representing the index) for the " + "discrete values, instead got: {}".format(type(d))) + data_tmp.append(d) + + # take the corresponding "continuous" data + data = np.array([self.discrete_values[d] for d in data]) + + self._write_continuous(data) + + def _write_continuous(self, data): + """ + Write the given continuous data. Child method that has to be implement in the child classes. + + Args: + data (np.ndarray): continuous data to be written. + """ + raise NotImplementedError def __copy__(self): """Return a shallow copy of the action. This can be overridden in the child class.""" @@ -205,9 +254,10 @@ class JointPositionAction(JointAction): """Return the joint limits.""" return self.robot.get_joint_limits(self.joints) - def _write(self, data): + def _write_continuous(self, data): """apply the action data on the robot.""" - self.robot.set_joint_positions(data, self.joints, kp=self.kp, kd=self.kd, forces=self.max_force) + self.robot.set_joint_positions(data, self.joints, bounds=self.bounds, kp=self.kp, kd=self.kd, + forces=self.max_force, discrete_values=self.discrete_values) def __copy__(self): """Return a shallow copy of the action. This can be overridden in the child class.""" @@ -223,10 +273,13 @@ class JointPositionAction(JointAction): return memo[self] robot = memo.get(self.robot, self.robot) # copy.deepcopy(self.robot, memo) joints = copy.deepcopy(self.joints) + bounds = copy.deepcopy(self.bounds) kp = copy.deepcopy(self.kp) kd = copy.deepcopy(self.kd) max_force = copy.deepcopy(self.max_force) - action = self.__class__(robot=robot, joint_ids=joints, kp=kp, kd=kd, max_force=max_force) + discrete_values = copy.deepcopy(self.discrete_values) + action = self.__class__(robot=robot, joint_ids=joints, bounds=bounds, kp=kp, kd=kd, max_force=max_force, + discrete_values=discrete_values) memo[self] = action return action @@ -265,11 +318,11 @@ class JointPositionChangeAction(JointPositionAction): if self.discrete_values is None: self.data = np.zeros(len(self.joints)) - def _write(self, data): + def _write_continuous(self, data): """apply the action data on the robot.""" # add the original joint positions data += self.robot.get_joint_positions(self.joints) - super(JointPositionChangeAction, self)._write(data) + super(JointPositionChangeAction, self)._write_continuous(data) class JointVelocityAction(JointAction): @@ -305,12 +358,12 @@ class JointVelocityAction(JointAction): np.infty * np.ones(len(self.joints))]) self._space = gym.spaces.Box(low=bounds[:, 0], high=bounds[:, 1]) - def _write(self, data): + def _write_continuous(self, data): """apply the action data on the robot.""" self.robot.set_joint_velocities(data, self.joints) -class JointVelocityChangeAction(JointAction): +class JointVelocityChangeAction(JointVelocityAction): r"""Joint Velocity Change Action Set the joint velocities using velocity control; this class expect to receive a change in the joint velocities @@ -332,19 +385,20 @@ class JointVelocityChangeAction(JointAction): the first value along the first axis / dimension are the values by default that are set if no data is provided. """ - super(JointVelocityChangeAction, self).__init__(robot, joint_ids, discrete_values=discrete_values) + super(JointVelocityChangeAction, self).__init__(robot, joint_ids, bounds=bounds, + discrete_values=discrete_values) # set data if continuous if self.discrete_values is None: self.data = np.zeros(len(self.joints)) - def _write(self, data): + def _write_continuous(self, data): """apply the action data on the robot.""" data += self.robot.get_joint_velocities(self.joints) - super(JointVelocityChangeAction, self)._write(data) + super(JointVelocityChangeAction, self)._write_continuous(data) -class JointPositionAndVelocityAction(JointAction): +class JointPositionAndVelocityAction(JointAction): # TODO: discrete values r"""Joint position and velocity action Set the joint position using position control using PD control, where the constraint error to be minimized is @@ -377,7 +431,7 @@ class JointPositionAndVelocityAction(JointAction): self.data = np.concatenate((pos, vel)) self.idx = len(self.joints) - def _write(self, data): + def _write_continuous(self, data): """apply the action data on the robot.""" self.robot.set_joint_positions(data[:self.idx], self.joints, kp=self.kp, kd=self.kd, velocities=data[self.idx:], forces=self.max_force) @@ -434,11 +488,11 @@ class JointPositionAndVelocityChangeAction(JointPositionAndVelocityAction): if self.discrete_values is None: self.data = np.zeros(2*len(self.joints)) - def _write(self, data): + def _write_continuous(self, data): """apply the action data on the robot.""" pos, vel = self.robot.get_joint_positions(self.joints), self.robot.get_joint_velocities(self.joints) data += np.concatenate((pos, vel)) - super(JointPositionAndVelocityChangeAction, self)._write(data) + super(JointPositionAndVelocityChangeAction, self)._write_continuous(data) # class JointPositionVelocityAccelerationAction(JointAction): @@ -491,14 +545,15 @@ class JointTorqueAction(JointAction): self.data = robot.get_joint_torques(self.joints) self._space = gym.spaces.Box(low=self.f_min, high=self.f_max) - def _write(self, data): + def _write_continuous(self, data): """apply the action data on the robot.""" data = np.clip(data, self.f_min, self.f_max) + print(data) self.robot.set_joint_torques(data, self.joints) def __copy__(self): """Return a shallow copy of the action. This can be overridden in the child class.""" - return self.__class__(robot=self.robot, joint_ids=self.joints, f_min=self.f_min, f_max=self.f_max) + return self.__class__(robot=self.robot, joint_ids=self.joints, bounds=(self.f_min, self.f_max)) def __deepcopy__(self, memo={}): """Return a deep copy of the action. This can be overridden in the child class. @@ -550,11 +605,11 @@ class JointTorqueGravityCompensationAction(JointTorqueAction): if self.discrete_values is None: self.data = np.zeros(len(self.joints)) - def _write(self, data): + def _write_continuous(self, data): """apply the action data on the robot.""" # add gravity compensation torques data += self.robot.get_gravity_compensation_torques(q_idx=self.q_indices) - super(JointTorqueGravityCompensationAction, self)._write(data) + super(JointTorqueGravityCompensationAction, self)._write_continuous(data) # alias @@ -562,7 +617,7 @@ class JointTorqueGravityCompensationAction(JointTorqueAction): JointTorqueChangeAction = JointTorqueGravityCompensationAction -class JointAccelerationAction(JointAction): +class JointAccelerationAction(JointAction): # TODO: discrete values r"""Joint Acceleration Action Set the joint accelerations using force/torque control. In order to produce the given joint accelerations, @@ -595,14 +650,15 @@ class JointAccelerationAction(JointAction): if self.discrete_values is None: self.data = robot.get_joint_accelerations(self.joints) - def _write(self, data): + def _write_continuous(self, data): """apply the action data on the robot.""" data = np.clip(data, self.a_min, self.a_max) self.robot.set_joint_accelerations(data, self.joints) def __copy__(self): """Return a shallow copy of the action. This can be overridden in the child class.""" - return self.__class__(robot=self.robot, joint_ids=self.joints, a_min=self.a_min, a_max=self.a_max) + return self.__class__(robot=self.robot, joint_ids=self.joints, bounds=(self.a_min, self.a_max), + discrete_values=self.discrete_values) def __deepcopy__(self, memo={}): """Return a deep copy of the action. This can be overridden in the child class. @@ -616,6 +672,43 @@ class JointAccelerationAction(JointAction): joints = copy.deepcopy(self.joints) a_min = copy.deepcopy(self.a_min) a_max = copy.deepcopy(self.a_max) - action = self.__class__(robot=robot, joint_ids=joints, f_min=a_min, f_max=a_max) + discrete_values = copy.deepcopy(self.discrete_values) + action = self.__class__(robot=robot, joint_ids=joints, bounds=(a_min, a_max), discrete_values=discrete_values) memo[self] = action return action + + +class JointAccelerationChangeAction(JointAccelerationAction): + r"""Joint Acceleration Change Action + + Set the joint accelerations using force/torque control. In order to produce the given joint accelerations, + we use inverse dynamics which given the joint accelerations produce the corresponding joint forces/torques + to be applied. + """ + + def __init__(self, robot, joint_ids=None, bounds=(None, None), discrete_values=None): + """ + Initialize the joint acceleration action. + + Args: + robot (Robot): robot instance. + joint_ids (int, list of int, None): joint id, or list of joint ids. If None, get all the actuated joints. + bounds (tuple of 2 float / np.array[N] / None): minimum and maximum accelerations. If None, it will use + the default joint acceleration limits. If it still doesn't find them, it will set -np.infty and + np.infty. + discrete_values (np.array[M], np.array[N,M], list of np.array[M], None): discrete values for each joint. + Note that by specifying this, the joint action is no more continuous but becomes discrete. By default, + the first value along the first axis / dimension are the values by default that are set if no data + is provided. + """ + super(JointAccelerationChangeAction, self).__init__(robot, joint_ids, bounds=bounds, + discrete_values=discrete_values) + + # set data if continuous + if self.discrete_values is None: + self.data = np.zeros(len(self.joints)) + + def _write_continuous(self, data): + """apply the action data on the robot.""" + data += self.robot.get_joint_accelerations(self.joints) + super(JointAccelerationChangeAction, self)._write_continuous(data) diff --git a/pyrobolearn/actions/robot_actions/link_actions.py b/pyrobolearn/actions/robot_actions/link_actions.py index 89d8521..c2a157c 100644 --- a/pyrobolearn/actions/robot_actions/link_actions.py +++ b/pyrobolearn/actions/robot_actions/link_actions.py @@ -52,12 +52,12 @@ class LinkAction(RobotAction): # TODO: multiple links raise TypeError("Expecting the given 'discrete_values' to be a list/tuple/np.array of float/int, but " "instead got: {}".format(type(discrete_values))) discrete_values = np.asarray(discrete_values) - self._space = gym.spaces.Discrete(len(self.discrete_values)) + self._space = gym.spaces.Discrete(len(discrete_values)) self.discrete_values = discrete_values # set the data in the case it is discrete if self.discrete_values is not None: - self.data = 0 # set the data to be the first index + self.data = np.zeros(1, dtype=np.int) # set the data to be the first index def _check_discrete_values(self, dim, last_dim): """Check that the discrete values have the correct dimensions / shape.""" @@ -542,12 +542,13 @@ class ApplyForceAction(LinkAction): # TODO: multiple links super(ApplyForceAction, self).__init__(robot, link_id, discrete_values=discrete_values) # check local position - if not isinstance(local_position, (list, tuple, np.ndarray)): - raise TypeError("Expecting the given 'local_position' to be a list/tuple/np.array of 3 float, but instead " - "got: {}".format(type(local_position))) - if len(local_position) != 3: - raise ValueError("Expecting the given 'local_position' to be a list/tuple/np.array of 3 float, but " - "instead got a length of: {}".format(len(local_position))) + if local_position is not None: + if not isinstance(local_position, (list, tuple, np.ndarray)): + raise TypeError("Expecting the given 'local_position' to be a list/tuple/np.array of 3 float, or None, " + "but instead got: {}".format(type(local_position))) + if len(local_position) != 3: + raise ValueError("Expecting the given 'local_position' to be a list/tuple/np.array of 3 float, but " + "instead got a length of: {}".format(len(local_position))) self.local_position = local_position # check axis @@ -584,7 +585,7 @@ class ApplyForceAction(LinkAction): # TODO: multiple links """apply the action data on the robot.""" if self.axis is not None: data = data * self.axis - self.robot.apply_external_force(force=data, link_id=self.link[0], position=self.local_position) + self.robot.apply_external_force(force=data, link_id=self.link, position=self.local_position) class ApplyTorqueAction(LinkAction): # TODO: multiple links @@ -645,7 +646,7 @@ class ApplyTorqueAction(LinkAction): # TODO: multiple links """apply the action data on the robot.""" if self.axis is not None: data = data * self.axis - self.robot.apply_external_torque(torque=data, link_id=self.link[0]) + self.robot.apply_external_torque(torque=data, link_id=self.link) # class ApplyWrenchAction(LinkAction): # TODO: multiple links diff --git a/pyrobolearn/control/README.md b/pyrobolearn/control/README.md index 0c5cef7..96ad7a7 100644 --- a/pyrobolearn/control/README.md +++ b/pyrobolearn/control/README.md @@ -1,3 +1,5 @@ ## Control processes/algorithms +THIS IS UNDER CONSTRUCTION + This folder will contain in the future control processes/algorithms. diff --git a/pyrobolearn/controllers/README.md b/pyrobolearn/controllers/README.md index 2c7a35f..8acf5a8 100644 --- a/pyrobolearn/controllers/README.md +++ b/pyrobolearn/controllers/README.md @@ -1,5 +1,7 @@ ## Controllers +THIS IS UNDER CONSTRUCTION + Controllers are basically policies that do not possess any (hyper-)parameters to optimize. They are manually coded by the user. Planning (TODO): diff --git a/pyrobolearn/envs/control/acrobot.py b/pyrobolearn/envs/control/acrobot.py new file mode 100644 index 0000000..2f8bc90 --- /dev/null +++ b/pyrobolearn/envs/control/acrobot.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +"""Provide the acrobot environment. + +This is based on the control problem proposed in OpenAI Gym: +"The acrobot system includes two joints and two links, where the joint between the two links is actuated. Initially, +the links are hanging downwards, and the goal is to swing the end of the lower link up to a given height." [1] + +References: + - [1] Acrobot environment in OpenAI Gym: https://gym.openai.com/envs/Acrobot-v1/ + - [2] "Generalization in Reinforcement Learning: Successful Examples Using Sparse Coarse Coding", Sutton, 1996. +""" + +import numpy as np + +import pyrobolearn as prl +from pyrobolearn.envs.control.control import ControlEnv + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2019, PyRoboLearn" +__credits__ = ["OpenAI", "Brian Delhaisse"] +__license__ = "GNU GPLv3" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class AcrobotEnv(ControlEnv): + r"""Acrobot Environment + + This is based on the control problem proposed in OpenAI Gym [1]: + "The acrobot system includes two joints and two links, where the joint between the two links is actuated. + Initially, the links are hanging downwards, and the goal is to swing the end of the lower link up to a given + height." [1] + + Here are the various environment features: + + - world: basic world with gravity enabled, a basic floor and the acrobot. + - state: the state is given by :math:`[cos(q_1), sin(q_1), cos(q_2), sin(q_2), \dot{q}_1, \dot{q}_2]` + - action: discrete joint torques :math:`\tau_2 \in \{-1., 0., +1.\}` + - reward: -1 if not terminal + - initial state generator: initialize uniformly the joint position and velocity states between [-0.1, 0.1] + - physics randomizer: uniform distribution of the mass of the [mass - mass/10, mass + mass/10] + - terminal condition: if the end-effector link is above a certain height. + + References: + - [1] Acrobot environment in OpenAI Gym: https://gym.openai.com/envs/Acrobot-v1/ + - [2] "Generalization in Reinforcement Learning: Successful Examples Using Sparse Coarse Coding", Sutton, 1996. + """ + + def __init__(self, simulator=None, use_reward_shaping=False, verbose=False): + """ + Initialize the acrobot environment. + + Args: + simulator (Simulator): simulator instance. + use_reward_shaping (bool): if True, it will use a reward that guides how to achieve the goal. + verbose (bool): if True, it will print information when creating the environment. + """ + # create basic world + world = prl.worlds.BasicWorld(simulator) + robot = world.load_robot('acrobot') + robot.disable_motor() + if verbose: + robot.print_info() + + # create state: [cos(q_1), sin(q_1), cos(q_2), sin(q_2), \dot{q}_1, \dot{q}_2] + trig_position_state = prl.states.JointTrigonometricPositionState(robot=robot) + velocity_state = prl.states.JointVelocityState(robot=robot) + state = trig_position_state + velocity_state + if verbose: + print("\nObservation: {}".format(state)) + + # create action: \tau_2 in {0., -1., +1.} + action = prl.actions.JointTorqueAction(robot, joint_ids=robot.joints[-1], + discrete_values=np.array([0., -1., +1.])) + if verbose: + print("\nAction: {}".format(action)) + + # create terminal condition: + terminal_condition = prl.terminal_conditions.LinkPositionCondition(robot, link_id=robot.joints[-1], + bounds=(2.5, np.infty), dim=2, + out=True, stay=False) + + # create reward: -1 if not terminal + if use_reward_shaping: # use continuous reward + # distance_cost = prl.rewards.DistanceCost() + # orientation_cost = prl.rewards.OrientationCost() + position_cost = prl.rewards.JointPositionCost(prl.states.JointPositionState(robot), + target_state=np.zeros(len(robot.joints)), + update_state=True) + velocity_cost = prl.rewards.JointVelocityCost(velocity_state) + torque_cost = prl.rewards.JointTorqueCost(prl.states.JointForceTorqueState(robot=robot), update_state=True) + # reward = distance_cost + orientation_cost + 0.1 * velocity_cost + 0.01 * torque_cost + reward = position_cost + 0.1 * velocity_cost + 0.001 * torque_cost + else: # use discrete reward + reward = prl.rewards.TerminalReward(terminal_condition, subreward=-1., final_reward=0.) + + # create initial state generator: generate the state each time we reset the environment + def reset_robot(robot): # function to disable the motors every time we reset the joint state + def reset(): + robot.disable_motor() + return reset + + init_state = prl.states.JointPositionState(robot) + velocity_state + num_joints = len(robot.joints) + low = [[np.pi + 0.1] + [0]*(num_joints-1), [-0.1]*num_joints] + high = [[np.pi - 0.1] + [0]*(num_joints-1), [0.1]*num_joints] + initial_state_generator = prl.states.generators.UniformStateGenerator(state=init_state, low=low, high=high, + fct=reset_robot(robot)) + + # create physics randomizer: randomize the mass each time we reset the environment + masses = robot.get_link_masses(link_ids=robot.joints) + masses = (masses - masses / 10., masses + masses / 10.) + physics_randomizer = prl.physics.LinkPhysicsRandomizer(robot, link_ids=robot.joints, masses=masses) + + # create environment using composition + super(AcrobotEnv, self).__init__(world=world, states=state, rewards=reward, actions=action, + initial_state_generators=initial_state_generator, + physics_randomizers=physics_randomizer, terminal_conditions=terminal_condition) + + +# Test +if __name__ == "__main__": + from itertools import count + + # create simulator + sim = prl.simulators.Bullet() + + # create environment + env = AcrobotEnv(sim, verbose=True) + + # run simulation + env.reset() + for _ in count(): + env.step(sleep_dt=1./240) diff --git a/pyrobolearn/envs/control/cartpole.py b/pyrobolearn/envs/control/cartpole.py new file mode 100644 index 0000000..f4833f7 --- /dev/null +++ b/pyrobolearn/envs/control/cartpole.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python +"""Provide the inverted pole on a cart (Cartpole) environment. + +This is based on the control problem proposed in OpenAI Gym: +"A pole is attached by an un-actuated joint to a cart, which moves along a frictionless track. The system is +controlled by applying a force of +1 or -1 to the cart. The pendulum starts upright, and the goal is to prevent it +from falling over. A reward of +1 is provided for every timestep that the pole remains upright. The episode ends when +the pole is more than 15 degrees from vertical, or the cart moves more than 2.4 units from the center." [1] + +Note that compared to [1], you can specify the number of links that forms the inverted pole. + +References: + - [1] Cartpole environment in OpenAI Gym: https://gym.openai.com/envs/CartPole-v1/ + - [2] "Neuronlike Adaptive Elements That Can Solve Difficult Learning Control Problem", Barto et al., 1993. +""" + +import numpy as np + +import pyrobolearn as prl +from pyrobolearn.envs.control.control import ControlEnv + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2019, PyRoboLearn" +__credits__ = ["OpenAI", "Brian Delhaisse"] +__license__ = "GNU GPLv3" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class CartpoleEnv(ControlEnv): + r"""Cartpole Environment + + This is based on the control problem proposed in OpenAI Gym: + "A pole is attached by an un-actuated joint to a cart, which moves along a frictionless track. The system is + controlled by applying a force of +1 or -1 to the cart. The pendulum starts upright, and the goal is to prevent it + from falling over. A reward of +1 is provided for every timestep that the pole remains upright. The episode ends + when the pole is more than 15 degrees from vertical, or the cart moves more than 2.4 units from the center." [1] + + Note that compared to [1], you can specify the number of links that forms the inverted pole. + + Here are the various environment features (from [1]): + + - world: basic world with gravity enabled, a basic floor and the cartpole. + - state: the state is given by :math:`[x, \dot{x}, q_1, \dot{q}_1]` for one inverted pole with one link. + - action: discrete forces applied on the cart (+10., -10.) + - reward: +1 until termination step + - initial state generator: initialize uniformly the state with [-0.05, 0.05] + - physics randomizer: uniform distribution of the mass of the [mass - mass/10, mass + mass/10] + - terminal conditions: + - pole angle is more than 12 degrees + - cart position is more than 2.5m from the center + - episode length is greater than 200 steps + + References: + - [1] Cartpole environment in OpenAI Gym: https://gym.openai.com/envs/CartPole-v1/ + - [2] "Neuronlike Adaptive Elements That Can Solve Difficult Learning Control Problem", Barto et al., 1993. + """ + + def __init__(self, simulator=None, num_links=1, num_steps=200, verbose=True): + """ + Initialize the Cartpole environment. + + Args: + simulator (Simulator): simulator instance. + num_links (int): the number of links that forms the inverted pendulum. + verbose (bool): if True, it will print information when creating the environment. + """ + # create basic world + world = prl.worlds.World(simulator) + robot = prl.robots.CartPole(simulator, position=(0., 0., 0.), num_links=num_links, inverted_pole=False) + world.load_robot(robot) + robot.disable_motor(robot.joints) + if verbose: + robot.print_info() + + # create state: [x, \dot{x}, q_i, \dot{q}_i] + state = prl.states.JointPositionState(robot) + prl.states.JointVelocityState(robot) + if verbose: + print("\nState: {}".format(state)) + + # create action: f_cart = (-10., +10.) + action = prl.actions.JointForceAction(robot=robot, joint_ids=0, discrete_values=[-10., 10.]) + if verbose: + print("\nAction: {}".format(action)) + + # create terminal condition + pole_angle_condition = prl.terminal_conditions.JointPositionCondition(robot, joint_ids=1, + bounds=(-12 * np.pi/180, 12 * np.pi/180), + out=False, stay=True) + cart_position_condition = prl.terminal_conditions.LinkPositionCondition(robot, link_id=1, bounds=(-1., 1.), + dim=0, out=False, stay=True) + time_length_condition = prl.terminal_conditions.TimeLimitCondition(num_steps=num_steps) + terminal_conditions = [pole_angle_condition, cart_position_condition, time_length_condition] + + # create reward: +1 until termination step + reward = prl.rewards.TerminalReward(terminal_conditions=terminal_conditions, subreward=1., final_reward=1.) + if verbose: + print("\nReward: {}".format(state)) + + # create initial state generator: generate the state each time we reset the environment + def reset_robot(robot): # function to disable the motors every time we reset the joint state + def reset(): + robot.disable_motor(robot.joints) + return reset + + initial_state_generator = prl.states.generators.UniformStateGenerator(state=state, low=-0.05, high=0.05, + fct=reset_robot(robot)) + + # create environment using composition + super(CartpoleEnv, self).__init__(world=world, states=state, rewards=reward, actions=action, + terminal_conditions=terminal_conditions, + initial_state_generators=initial_state_generator) + + +# class CartDoublePoleEnv(Env): +# r"""CartDoublepole Environment +# +# This provide the double inverted poles on a cart environment. Compare to the standard inverted pendulum on a cart, +# the goal this time is to balance two poles of possibly different lengths / masses, which are initialized at +# different angles but connected at the same joint attached to the cart. +# """ +# +# def __init__(self, simulator=None, pole_lengths=(1., 1.), pole_masses=(1., 1.), pole_angles=(0., 0.)): +# """ +# Initialize the double inverted poles on a cart environment. +# +# Args: +# simulator (Simulator): simulator instance. +# """ +# # create basic world +# world = prl.worlds.BasicWorld(simulator) +# robot = prl.robots.CartDoublePole(simulator, pole_lengths=pole_lengths, pole_masses=pole_masses, +# pole_angles=pole_angles) +# world.load_robot(robot) +# +# # create state +# state = +# +# # create action +# action = +# +# # create reward +# reward = +# +# # create terminal condition +# terminal_condition = +# +# # create initial state generator +# initial_state_generator = +# +# # create environment using composition +# super(CartDoublePoleEnv, self).__init__(world=world, states=state, rewards=reward, actions=action) + + +# Test +if __name__ == "__main__": + from itertools import count + + # create simulator + sim = prl.simulators.Bullet() + + # create environment + env = CartpoleEnv(sim) + + state = env.reset() + # run simulation + for _ in count(): + state, reward, done, info = env.step(sleep_dt=1./240) + print("done: {}, reward: {}, state: {}".format(done, reward, state)) + + # # create basic world + # sim = prl.simulators.Bullet() + # world = prl.worlds.World(sim) + # robot = prl.robots.CartPole(sim, num_links=1, inverted_pole=True) + # robot.disable_motor(robot.joints) + # world.load_robot(robot) + # + # # create state: [x, \dot{x}, q_i, \dot{q}_i] + # state = prl.states.JointPositionState(robot) + prl.states.JointVelocityState(robot) + # + # # create action + # action = prl.actions.JointForceAction(robot=robot, joint_ids=0, discrete_values=[-10., 10.]) + # + # flip = 1 + # for i in prl.count(): + # # if i % 10 == 0: + # # flip = (flip+1) % 2 + # action(flip) + # world.step(sleep_dt=sim.dt) diff --git a/pyrobolearn/envs/env.py b/pyrobolearn/envs/env.py index 0647a8d..dbc16a5 100644 --- a/pyrobolearn/envs/env.py +++ b/pyrobolearn/envs/env.py @@ -205,7 +205,7 @@ class Env(object): # gym.Env): # TODO: make it inheriting the gym.Env conditions = [conditions] elif isinstance(conditions, (list, tuple)): for idx, condition in enumerate(conditions): - if not callable(conditions): + if not isinstance(condition, TerminalCondition): raise TypeError("Expecting the {} item in the given terminal conditions to be an instance of " "`TerminalCondition`, instead got: {}".format(idx, type(condition))) else: @@ -293,7 +293,7 @@ class Env(object): # gym.Env): # TODO: make it inheriting the gym.Env # generate initial states for generator in self.state_generators: - generator() + generator(reset_state=False) # reset states and return first states/observations states = [state.reset() for state in self.states] diff --git a/pyrobolearn/physics/joint_physics_randomizer.py b/pyrobolearn/physics/joint_physics_randomizer.py index 1a9e1a3..8182413 100644 --- a/pyrobolearn/physics/joint_physics_randomizer.py +++ b/pyrobolearn/physics/joint_physics_randomizer.py @@ -194,8 +194,8 @@ class JointPhysicsRandomizer(BodyPhysicsRandomizer): # sample each property properties = dict() - for joint in self.joints: + for i, joint in enumerate(self.joints): for name, bound in zip(self.names(), self.bounds()): if bound is not None: - properties.setdefault(joint, {})[name] = np.random.uniform(low=bound[0], high=bound[1]) + properties.setdefault(joint, {})[name] = np.random.uniform(low=bound[0][i], high=bound[1][i]) return properties diff --git a/pyrobolearn/physics/link_physics_randomizer.py b/pyrobolearn/physics/link_physics_randomizer.py index 1864fa2..509aedc 100644 --- a/pyrobolearn/physics/link_physics_randomizer.py +++ b/pyrobolearn/physics/link_physics_randomizer.py @@ -429,8 +429,8 @@ class LinkPhysicsRandomizer(BodyPhysicsRandomizer): # sample each property properties = dict() - for link in self.links: + for i, link in enumerate(self.links): for name, bound in zip(self.names(), self.bounds()): if bound is not None: - properties.setdefault(link, {})[name] = np.random.uniform(low=bound[0], high=bound[1]) + properties.setdefault(link, {})[name] = np.random.uniform(low=bound[0][i], high=bound[1][i]) return properties diff --git a/pyrobolearn/priorities/README.rst b/pyrobolearn/priorities/README.rst index 6e4154d..c93e609 100644 --- a/pyrobolearn/priorities/README.rst +++ b/pyrobolearn/priorities/README.rst @@ -1,6 +1,8 @@ Priority Tasks ============== +THIS IS UNDER CONSTRUCTION + In this folder, you will find the code for priority "tasks". The "tasks" defined here are different from the tasks defined in the ``pyrobolearn/tasks`` folder which defines robot learning tasks. The tasks defined here can be more seen as "constraints"; for instance, the constraint for the robot to maintain its balance (i.e. have its center of mass diff --git a/pyrobolearn/robots/acrobot.py b/pyrobolearn/robots/acrobot.py index c82f0c8..12ad466 100644 --- a/pyrobolearn/robots/acrobot.py +++ b/pyrobolearn/robots/acrobot.py @@ -50,6 +50,8 @@ class Acrobot(Robot): # TODO: create the acrobot dynamically instead of loading super(Acrobot, self).__init__(simulator, urdf, position, orientation, fixed_base, scale) self.name = 'acrobot' + self.height = 2. * scale + # set initial joint positions self.reset_joint_states(q=[np.pi, 0.], joint_ids=self.joints) diff --git a/pyrobolearn/robots/actuators/actuator.py b/pyrobolearn/robots/actuators/actuator.py index bde8af1..2ecdd96 100644 --- a/pyrobolearn/robots/actuators/actuator.py +++ b/pyrobolearn/robots/actuators/actuator.py @@ -7,6 +7,8 @@ other joint actuators. Additionally, this is important as more realistic motors simulation to reality. """ +# TODO: add latency + noise + __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] @@ -24,10 +26,17 @@ class Actuator(object): Other actuators such as speakers, leds, and others are attached to links. """ - def __init__(self): + def __init__(self, latency=0): + """ + Initialize the actuator. + + Args: + latency (int, float): latency. + """ # variable to check if the actuator is enabled self._enabled = True + self._latency = latency # self.sim = simulator # diff --git a/pyrobolearn/robots/cartpole.py b/pyrobolearn/robots/cartpole.py index cd5d0d0..cb0320d 100644 --- a/pyrobolearn/robots/cartpole.py +++ b/pyrobolearn/robots/cartpole.py @@ -106,8 +106,6 @@ class CartPole(Robot): dims = (15, 0.025, 0.025) color = (0, 0.8, 0.8, 1) mass = 0 - position = (0, 0, 0) - orientation = (0, 0, 0, 1) collision_shape = self.sim.create_collision_shape(self.sim.GEOM_BOX, half_extents=dims) visual_shape = self.sim.create_visual_shape(self.sim.GEOM_BOX, half_extents=dims, rgba_color=color) diff --git a/pyrobolearn/robots/pendulum.py b/pyrobolearn/robots/pendulum.py index 30075b9..ab7e4b4 100644 --- a/pyrobolearn/robots/pendulum.py +++ b/pyrobolearn/robots/pendulum.py @@ -50,6 +50,8 @@ class Pendulum(Robot): # TODO: create the pendulum dynamically instead of loadi super(Pendulum, self).__init__(simulator, urdf, position, orientation, fixed_base, scale) self.name = 'pendulum' + self.height = 2. * scale + # set initial joint positions self.reset_joint_states(q=[np.pi / 4], joint_ids=self.joints) diff --git a/pyrobolearn/robots/robot.py b/pyrobolearn/robots/robot.py index 4f5eb4f..487c5a4 100644 --- a/pyrobolearn/robots/robot.py +++ b/pyrobolearn/robots/robot.py @@ -174,6 +174,15 @@ class Robot(ControllableBody): # Operators # ############# + # def __str__(self): + # """Return a string describing the robot.""" + # return "\nRobot: {} \nNumber of DoFs: {} \nJoint ids: {} \nActuated joint ids: {} " \ + # "\nLink names (associated with actuated joints): {} \nEnd-effector names: {} \nFloating base? {} " \ + # "\nTotal mass = {} kg".format(self.__class__.__name__, self.num_dofs, list(range(self.num_joints)), + # self.joints, self.get_link_names(self.joints), + # self.get_link_names(self.end_effectors), self.has_floating_base(), + # self.mass) + def __copy__(self): """Return a shallow copy of the robot. This can be overridden in the child class.""" return self.__class__(simulator=self.simulator, urdf=self.urdf, position=self.position, @@ -1226,21 +1235,38 @@ class Robot(ControllableBody): # check q if q is None: - q = np.zeros(len(joint_ids)) + if 'q_reset' in self._state: + q = self._state['q_reset'] + else: + q = np.zeros(len(joint_ids)) elif isinstance(q, (int, float)): q = [q] + self._state['q_reset'] = q else: if len(q) != len(joint_ids): raise ValueError("The number of joint ids does not match up with the number of q's") + self._state['q_reset'] = q # check dq if dq is None: - dq = np.zeros(len(joint_ids)) + if 'dq_reset' in self._state: + dq = self._state['dq_reset'] + else: + dq = np.zeros(len(joint_ids)) elif isinstance(dq, (int, float)): dq = [dq] + self._state['dq_reset'] = dq else: if len(dq) != len(joint_ids): raise ValueError("The number of joint ids does not match with the number of dq's") + self._state['dq_reset'] = dq + + # import inspect + # stack = inspect.stack() + # the_class = stack[1][0].f_locals["self"].__class__ + # the_method = stack[1][0].f_code.co_name + # print("I was called by {}.{}()".format(str(the_class), the_method)) + # print("resetting: {}, {}, {}".format(joint_ids, q, dq)) # reset the joint state for joint_id, position, velocity in zip(joint_ids, q, dq): @@ -4457,7 +4483,7 @@ class Robot(ControllableBody): pos = self.get_link_world_positions(link) dim = self.visual_shapes[link]['dimensions'] # radius = min(dim) * scaling * 0.2 - radius = 0.01 + radius = 0.01 * scaling self._draw_sphere(pos, radius, color=(0, 0, 0, 1)) def draw_link_frames(self, link_ids=None, scaling=1.): diff --git a/pyrobolearn/robots/sensors/sensor.py b/pyrobolearn/robots/sensors/sensor.py index 1151c7b..e48050c 100644 --- a/pyrobolearn/robots/sensors/sensor.py +++ b/pyrobolearn/robots/sensors/sensor.py @@ -8,6 +8,8 @@ simulation to reality. Also, note that some simulators are deterministic and thu add some noise to the returned sense value. The type of noise can also be selected at runtime. """ +# TODO: add latency + noise + import copy from abc import ABCMeta, abstractmethod import numpy as np @@ -34,7 +36,7 @@ class Sensor(object): # sensor attached to a link or joint """ __metaclass__ = ABCMeta - def __init__(self, simulator, body_id, position=None, orientation=None, rate=1): + def __init__(self, simulator, body_id, position=None, orientation=None, rate=1, latency=0): """Initialize the sensor. Args: @@ -43,6 +45,7 @@ class Sensor(object): # sensor attached to a link or joint position (vec3): local position of the sensor with respect to the given link orientation (vec4): local orientation of the sensor with respect to the given link rate (int): number of steps to wait before acquisition of the next sensor value. + latency (int, float): latency. """ self.sim = simulator self.body_id = body_id @@ -63,6 +66,7 @@ class Sensor(object): # sensor attached to a link or joint # variable to check if the sensor is enabled self._enabled = True + self._latency = latency ############## # Properties # diff --git a/pyrobolearn/states/generators/state_generator.py b/pyrobolearn/states/generators/state_generator.py index 3d39940..e240c23 100644 --- a/pyrobolearn/states/generators/state_generator.py +++ b/pyrobolearn/states/generators/state_generator.py @@ -106,9 +106,9 @@ class StateGenerator(object): """Return a string describing the class.""" return self.__class__.__name__ - def __call__(self, set_data=True): + def __call__(self, set_data=True, reset_state=True): """Call the generator.""" - return self.generate(set_data=set_data) + return self.generate(set_data=set_data, reset_state=reset_state) class FixedStateGenerator(StateGenerator): @@ -474,10 +474,11 @@ class UniformStateGenerator(StateDistributionGenerator): # for idx, datum, low, high in enumerate(zip(data, self.low, self.high)): # data[idx] = np.clip(datum, low, high) - data = [np.random.uniform(low=low, high=high, size=len(state)) + data = [np.random.uniform(low=low, high=high, size=state.total_size()) for state, low, high in zip(self.state, self.low, self.high)] if set_data: self.state.data = data + # print("Generate: {}".format(self.state.data)) if reset_state: self.state.reset() return data diff --git a/pyrobolearn/terminal_conditions/__init__.py b/pyrobolearn/terminal_conditions/__init__.py index 28b9cce..a5b1f9b 100644 --- a/pyrobolearn/terminal_conditions/__init__.py +++ b/pyrobolearn/terminal_conditions/__init__.py @@ -1,3 +1,19 @@ # import terminal conditions -from .terminal_condition import * +from .terminal_condition import TerminalCondition, HasFallenCondition, HasReachedCondition + +# import basic terminal conditions +from .basic_conditions import TimeLimitCondition, GymTerminalCondition + +# import body terminal condition +from .body_conditions import BodyCondition, PositionCondition, OrientationCondition + +# import robot terminal condition +from .robot_condition import RobotCondition + +# import joint conditions +from .joint_conditions import JointCondition, JointPositionCondition, JointVelocityCondition, \ + JointAccelerationCondition, JointTorqueCondition + +# import link conditions +from .link_conditions import LinkCondition, LinkPositionCondition, LinkOrientationCondition diff --git a/pyrobolearn/terminal_conditions/basic_conditions.py b/pyrobolearn/terminal_conditions/basic_conditions.py new file mode 100644 index 0000000..43ef3fc --- /dev/null +++ b/pyrobolearn/terminal_conditions/basic_conditions.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +"""Define some common terminal conditions for the environment. +""" + +import copy +import numpy as np + +from pyrobolearn.terminal_conditions.terminal_condition import TerminalCondition + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2019, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "GNU GPLv3" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class TimeLimitCondition(TerminalCondition): + r"""Time Limit Terminal Condition + + """ + + def __init__(self, num_steps, btype='neutral'): + """ + Initialize the time limit terminal condition. + + Args: + num_steps (int): + btype (bool, str, None): if the terminal condition represents a failure or success condition. If None, it + represents a neutral terminal condition (which is neither a failure or success condition, but just + means the episode is over). If string, it has to be among {"success", "failure", "neutral"}. + """ + super(TimeLimitCondition, self).__init__(btype=btype) + self.num_steps = num_steps + self.cnt = 0 + + def reset(self): + """ + Reset the terminal condition. + """ + self.cnt = 0 + + def check(self): + """ + Check if the terminating condition has been fulfilled, and return True or False accordingly + """ + if self.cnt >= self.num_steps: + self._over = True + return self._over + self.cnt += 1 + return self._over + + +class GymTerminalCondition(TerminalCondition): + r"""OpenAI Gym Terminal Condition + + Returns if the OpenAI Gym environment has terminated. This does not provide any information if the environment + terminated because the policy succeeded or failed to perform the task. + """ + + def __init__(self, done=False): + """ + Initialize the Gym terminal condition. + + Args: + done (bool): True if done. + """ + super(GymTerminalCondition, self).__init__(btype='neutral') # None because we don't know + self._value = done + + def check(self): + """Check if the terminating condition has been fulfilled, and return True or False accordingly""" + return self._value + + +class FixedTerminalCondition(TerminalCondition): + r"""Fixed terminal condition + + Dummy fixed terminal condition. + """ + + def __init__(self, value=False, btype=None, name=None): + """ + Initialize the dummy fixed terminal condition. + + Args: + value (bool): if the terminal condition is over or not. This can be set by the user. + btype (bool, str, None): if the terminal condition represents a failure or success condition. If None, it + represents a neutral terminal condition (which is neither a failure or success condition, but just + means the episode is over). If string, it has to be among {"success", "failure", "neutral"}. + name (str): name of the final condition + """ + super(FixedTerminalCondition, self).__init__(btype=btype, name=name) + self._over = value + + @property + def value(self): + return self._over + + @value.setter + def value(self, value): + self._over = bool(value) + + +class FunctionalTerminalCondition(TerminalCondition): + r"""Functional terminal condition + + Terminal condition that calls the given function every time we check if the task was carried out or not. + """ + + def __init__(self, fct, btype=None, name=None): + """ + Initialize the functional terminal condition. + + Args: + fct (callable): function to call each time we check if the task was carried out or not. + btype (bool, str, None): if the terminal condition represents a failure or success condition. If None, it + represents a neutral terminal condition (which is neither a failure or success condition, but just + means the episode is over). If string, it has to be among {"success", "failure", "neutral"}. + name (str): name of the final condition + """ + if not callable(fct): + raise TypeError("Expecting the provided 'fct' to be callable, but received instead: {}".format(fct)) + self._fct = fct + + def check(self): + """ + Check if the terminating condition has been fulfilled, and return True or False accordingly + """ + answer = self._fct() + if isinstance(answer, tuple) and len(answer) == 2: + self._over, self.btype = answer + return self._over + + +# Tests +if __name__ == '__main__': + + condition = TimeLimitCondition(num_steps=2, btype='failure') + + for i in range(4): + cond = condition() + print("Iter: {}, type={}, over={}".format(i, condition.type_str(), condition.is_over())) diff --git a/pyrobolearn/terminal_conditions/body_conditions.py b/pyrobolearn/terminal_conditions/body_conditions.py new file mode 100644 index 0000000..d398c6e --- /dev/null +++ b/pyrobolearn/terminal_conditions/body_conditions.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python +"""Define some body terminal conditions for the environment. +""" + +from abc import ABCMeta +import numpy as np + +from pyrobolearn.robots.base import Body +from pyrobolearn.terminal_conditions.terminal_condition import TerminalCondition +from pyrobolearn.utils.transformation import get_rpy_from_quaternion + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2019, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "GNU GPLv3" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class BodyCondition(TerminalCondition): + r"""Body Terminal Condition + + This terminal condition describes 8 cases: + + 1. all the dimensions of the body state are: + 1. in a certain bounds and must stay between these bounds. Once one gets out, the terminal condition is over, + and results in a failure. (all=True, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once they all get out, the terminal condition is over, + and results in a success. (all=True, out=False, stay=False) + 3. outside a certain bounds and must get in. Once they all get in, the terminal condition is over, and results + in a success. (all=True, out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once one gets in, the terminal condition is over, + and results in a failure. (all=True, out=True, stay=True) + 2. any of the dimension of the body state is: + 1. in a certain bounds and must stay between these bounds. Once they all get out, the terminal condition is + over, and results in a failure. (all=False, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once one gets out, the terminal condition is over, + and results in a success. (all=False, out=False, stay=False) + 3. outside a certain bounds and must get in. Once one gets in, the terminal condition is over, and results in + a success. (all=False ,out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once they all get in, the terminal condition is + over, and results in a failure. (all=False, out=True, stay=True) + + Body state includes the position and orientation for instance. + """ + __metaclass__ = ABCMeta + + def __init__(self, body, bounds, dim=None, out=False, stay=False, all=False): + """ + Initialize the body terminal condition. + + Args: + body (Body): body instance + dim (None, int, int[3]): dimensions that we should consider when looking at the bounds. If None, it will + consider all 3 dimensions. If one dimension is provided it will only check along that dimension. If + a np.array of 0 and 1 is provided, it will consider the dimensions that are equal to 1. Thus, [1,0,1] + means to consider the bounds along the x and z axes. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the state + leave the bounds it results in a failure. if :attr:`stay` is False, it must get outside these bounds; + if the state leaves the bounds, it results in a success. + all (bool): this is only used if they are multiple dimensions. if True, all the dimensions of the state + are checked if they are inside or outside the bounds depending on the other parameters. if False, any + dimensions will be checked. + """ + super(BodyCondition, self).__init__() + self.body = body + self.dim = dim + self.bounds = bounds + self._out = bool(out) + self._stay = bool(stay) + self._all = bool(all) + + ############## + # Properties # + ############## + + @property + def body(self): + """Return the body instance.""" + return self._body + + @body.setter + def body(self, body): + """Set the body instance.""" + if not isinstance(body, Body): + raise TypeError("Expecting the given 'body' to be an instance of `Body`, instead got: " + "{}".format(type(body))) + self._body = body + + @property + def dim(self): + """Return the dimension(s).""" + return self._dim + + @dim.setter + def dim(self, dim): + """Set the dimensions.""" + if dim is not None: + if not isinstance(dim, (int, np.ndarray)): + raise TypeError("Expecting the given 'dim' to be an int or an np.array of 3 int, but got instead: " + "{}".format(type(dim))) + if isinstance(dim, np.ndarray): + if dim.size != 3: + raise ValueError("Expecting the given 'dim' np.array to be of size 3, but got instead a size of: " + "{}".format(dim.size)) + dim = np.array([bool(d) for d in dim]) + self._dim = dim + + ########### + # Methods # + ########### + + def _check_bounds(self, bounds): + """Check the given bounds.""" + # check the type of the bounds + if not isinstance(bounds, (tuple, list, np.ndarray)): + raise TypeError("Expecting the given bounds to be a tuple/list/np.ndarray of float, instead got: " + "{}".format(type(bounds))) + + # check that the bounds have a length of 2 (i.e. lower and upper bounds) + if len(bounds) != 2: + raise ValueError("Expecting the bounds to be of length 2 (i.e. lower and upper bounds), instead got a " + "length of {}".format(len(bounds))) + + # if one of the bounds is None, raise error + if bounds[0] is None or bounds[1] is None: + raise ValueError("Expecting the bounds to not have None, but got: {}".format(bounds)) + + # reshape bounds if necessary + bounds = np.asarray(bounds).reshape(2, -1) + + if self.dim is None: + if bounds.shape[1] != 3: + raise ValueError("Expecting the bounds to be of shape (2,3) but got instead a shape of: " + "{}".format(bounds.shape)) + else: + if isinstance(self.dim, int) and bounds.shape[1] != 1: + raise ValueError("If you specified one dimension, we expect the shape of the bounds to be (2,1), but " + "got instead a shape of: {}".format(bounds.shape)) + elif isinstance(self.dim, np.ndarray): + if bounds.shape[1] != len(self.dim[self.dim]): + raise ValueError("Expecting each bound to have the same number of elements than the elements that " + "are not zero in the given 'dim' attribute") + return bounds + + def check(self): + """ + Check if the terminating condition has been fulfilled, and return True or False accordingly + """ + states = self._get_states() + + if self._all: # all the dimension states + if self._out: # are outside a certain bounds + if self._stay: # and must stay outside these ones. + if np.any(self.bounds[0] <= states <= self.bounds[1]): # one dimension went inside + self._btype = False # failure + self._over = True # it is over + else: # they are still all outside + self._btype = True # success + self._over = False # it is not over + else: # and must go inside these ones + if np.all(self.bounds[0] <= states <= self.bounds[1]): # they all went inside + self._btype = True # success + self._over = True # it is over + else: # they are some still left outside + self._btype = False # failure + self._over = False # it is not over + else: # are inside a certain bounds + if self._stay: # and must stay inside these ones. + if not np.all(self.bounds[0] <= states <= self.bounds[1]): # one dimension went outside + self._btype = False # failure + self._over = True # it is over + else: # they are still all inside + self._btype = True # success + self._over = False # it is not over + else: # and must go outside these ones. + if np.any(self.bounds[0] <= states <= self.bounds[1]): # they are still some inside + self._btype = False # failure + self._over = False # it is not over + else: # they are all outside + self._btype = True # success + self._over = True # it is over + + else: # any of the dimension states + if self._out: # is outside a certain bounds + if self._stay: # and still stays outside these ones. + if not np.all(self.bounds[0] <= states <= self.bounds[1]): # at least one dim. is still outside + self._btype = True # success + self._over = False # it is not over + else: # they are all inside + self._btype = False # failure + self._over = True # it is over + else: # and one must at least go inside these ones + if np.any(self.bounds[0] <= states <= self.bounds[1]): # at least one state is inside + self._btype = True # success + self._over = True # it is over + else: # they are still all outside + self._btype = False # failure + self._over = False # it is not over + else: # is inside a certain bounds + if self._stay: # and must stay inside these ones. + if np.any(self.bounds[0] <= states <= self.bounds[1]): # at least one state is still inside + self._btype = True # success + self._over = False # it is not over + else: # they are all outside + self._btype = False # failure + self._over = True # it is over + else: # and must go outside these ones. + if np.all(self.bounds[0] <= states <= self.bounds[1]): # they are all inside + self._btype = False # failure + self._over = False # it is not over + else: # at least one went outside + self._btype = True # success + self._over = True # it is over + + return self._over + + def _get_states(self): + """Get the base states. Has to be implemented in the child class.""" + raise NotImplementedError + + +class PositionCondition(BodyCondition): + r"""World position terminal condition + + This terminal condition describes 8 cases: + + 1. all the dimensions of the body position state are: + 1. in a certain bounds and must stay between these bounds. Once one gets out, the terminal condition is over, + and results in a failure. (all=True, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once they all get out, the terminal condition is over, + and results in a success. (all=True, out=False, stay=False) + 3. outside a certain bounds and must get in. Once they all get in, the terminal condition is over, and results + in a success. (all=True, out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once one gets in, the terminal condition is over, + and results in a failure. (all=True, out=True, stay=True) + 2. any of the dimension of the body position state is: + 1. in a certain bounds and must stay between these bounds. Once they all get out, the terminal condition is + over, and results in a failure. (all=False, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once one gets out, the terminal condition is over, + and results in a success. (all=False, out=False, stay=False) + 3. outside a certain bounds and must get in. Once one gets in, the terminal condition is over, and results in + a success. (all=False ,out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once they all get in, the terminal condition is + over, and results in a failure. (all=False, out=True, stay=True) + """ + + def __init__(self, body, bounds=(None, None), dim=None, out=False, stay=False, all=False): + """ + Initialize the world position terminal condition. + + Args: + body (Body): body instance. + bounds (tuple of 2 float / np.array[3]): bounds on the body position. + dim (None, int, int[3]): dimensions that we should consider when looking at the bounds. If None, it will + consider all 3 dimensions. If one dimension is provided it will only check along that dimension. If + a np.array of 0 and 1 is provided, it will consider the dimensions that are equal to 1. Thus, [1,0,1] + means to consider the bounds along the x and z axes. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the position + leaves the bounds it results in a failure. if :attr:`stay` is False, it must get outside these bounds; + if the position leaves the bounds, it results in a success. + all (bool): this is only used if they are multiple dimensions. if True, all the dimensions of the state + are checked if they are inside or outside the bounds depending on the other parameters. if False, any + dimensions will be checked. + """ + super(PositionCondition, self).__init__(body, bounds=bounds, dim=dim, out=out, stay=stay, all=all) + # check the bounds + self.bounds = self._check_bounds(bounds=bounds) + + def _get_states(self): + """Return the state.""" + position = self.body.position + if self.dim is None: + print(position) + return position + + print(position[self.dim]) + return position[self.dim] + + +class OrientationCondition(BodyCondition): + r"""World orientation terminal condition + + This terminal condition describes 8 cases: + + 1. all the dimensions of the body orientation (expressed as roll-pitch-yaw angles) state are: + 1. in a certain bounds and must stay between these bounds. Once one gets out, the terminal condition is over, + and results in a failure. (all=True, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once they all get out, the terminal condition is over, + and results in a success. (all=True, out=False, stay=False) + 3. outside a certain bounds and must get in. Once they all get in, the terminal condition is over, and results + in a success. (all=True, out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once one gets in, the terminal condition is over, + and results in a failure. (all=True, out=True, stay=True) + 2. any of the dimension of the body orientation (expressed as roll-pitch-yaw angles) state is: + 1. in a certain bounds and must stay between these bounds. Once they all get out, the terminal condition is + over, and results in a failure. (all=False, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once one gets out, the terminal condition is over, + and results in a success. (all=False, out=False, stay=False) + 3. outside a certain bounds and must get in. Once one gets in, the terminal condition is over, and results in + a success. (all=False ,out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once they all get in, the terminal condition is + over, and results in a failure. (all=False, out=True, stay=True) + """ + + def __init__(self, body, bounds=(None, None), dim=None, out=False, stay=False, all=False): + """ + Initialize the world position terminal condition. + + Args: + body (Body): body instance. + bounds (tuple of 2 float / np.array[3]): bounds on the body orientation expressed as roll-pitch-yaw angles + or axis-angle if the :attr:`axis` is provided. + dim (None, int, int[3]): dimensions that we should consider when looking at the bounds. If None, it will + consider all 3 dimensions. If one dimension is provided it will only check along that dimension. If + a np.array of 0 and 1 is provided, it will consider the dimensions that are equal to 1. Thus, [1,0,1] + means to consider the bounds along the x (roll) and z (yaw) axes. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the orientation + leaves the bounds it results in a failure. if :attr:`stay` is False, it must get outside these bounds; + if the orientation leaves the bounds, it results in a success. + all (bool): this is only used if they are multiple dimensions. if True, all the dimensions of the state + are checked if they are inside or outside the bounds depending on the other parameters. if False, any + dimensions will be checked. + """ + super(OrientationCondition, self).__init__(body, bounds=bounds, dim=dim, out=out, stay=stay, all=all) + # check the bounds + self.bounds = self._check_bounds(bounds=bounds) + + def _get_states(self): + """Return the state.""" + orientation = get_rpy_from_quaternion(self.body.orientation) + if self.dim is None: + return orientation + return orientation[self.dim] diff --git a/pyrobolearn/terminal_conditions/joint_conditions.py b/pyrobolearn/terminal_conditions/joint_conditions.py new file mode 100644 index 0000000..090d643 --- /dev/null +++ b/pyrobolearn/terminal_conditions/joint_conditions.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python +"""Define some joint terminal conditions for the environment. +""" + +import copy +import numpy as np +from abc import ABCMeta + +from pyrobolearn.terminal_conditions.robot_condition import RobotCondition + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2019, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "GNU GPLv3" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class JointCondition(RobotCondition): + r"""Joint Terminal Condition + + This terminal condition describes 8 cases: + + 1. all the joint states are: + 1. in a certain bounds and must stay between these bounds. Once one gets out, the terminal condition is over, + and results in a failure. (all=True, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once they all get out, the terminal condition is over, + and results in a success. (all=True, out=False, stay=False) + 3. outside a certain bounds and must get in. Once they all get in, the terminal condition is over, and results + in a success. (all=True, out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once one gets in, the terminal condition is over, + and results in a failure. (all=True, out=True, stay=True) + 2. any of the joint states is: + 1. in a certain bounds and must stay between these bounds. Once they all get out, the terminal condition is + over, and results in a failure. (all=False, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once one gets out, the terminal condition is over, + and results in a success. (all=False, out=False, stay=False) + 3. outside a certain bounds and must get in. Once one gets in, the terminal condition is over, and results in + a success. (all=False ,out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once they all get in, the terminal condition is + over, and results in a failure. (all=False, out=True, stay=True) + + States include positions, velocities, accelerations and torques. + """ + __metaclass__ = ABCMeta + + def __init__(self, robot, joint_ids=None, bounds=(None, None), out=False, stay=False, all=False): + """ + Initialize the joint terminal condition. + + Args: + robot (Robot): robot instance + joint_ids (int, int[N], None): joint id or list of joint ids + bounds (tuple of float / np.array[N]): bounds to stay in/out or reach/leave. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the joint state + leaves the bounds it results in a failure. if :attr:`stay` is False, it must get outside these bounds; + if the joint state leaves the bounds, it results in a success. + all (bool): this is only used if they are multiple joints. if True, all the joints are checked such that + they are inside or outside the bounds depending on the other parameters. if False, any joints will be + checked. + """ + super(JointCondition, self).__init__(robot, bounds=bounds, dim=None, out=out, stay=stay, all=all) + + # get the joints of the robot + if joint_ids is None: + joint_ids = robot.get_joint_ids() + elif isinstance(joint_ids, int): + joint_ids = [joint_ids] + self.joints = joint_ids + + # check the bounds + self.bounds = self._check_bounds(bounds=bounds) + + def _check_bounds(self, bounds): + """Check the given bounds.""" + # check the type of the bounds + if not isinstance(bounds, (tuple, list, np.ndarray)): + raise TypeError("Expecting the given bounds to be a tuple/list/np.ndarray of float, instead got: " + "{}".format(type(bounds))) + + # check that the bounds have a length of 2 (i.e. lower and upper bounds) + if len(bounds) != 2: + raise ValueError("Expecting the bounds to be of length 2 (i.e. lower and upper bounds), instead got a " + "length of {}".format(len(bounds))) + + # if one of the bounds is None, raise error + if bounds[0] is None or bounds[1] is None: + raise ValueError("Expecting the bounds to not have None, but got: {}".format(bounds)) + + # reshape bounds if necessary + bounds = np.asarray(bounds).reshape(2, -1) + if len(self.joints) != bounds.shape[1]: + if bounds.shape[1] == 1: + bounds = np.array([bounds[0, 0] * np.ones(len(self.joints)), + bounds[1, 0] * np.ones(len(self.joints))]) + else: + raise ValueError("Expecting the number of bounds (={}) to match up with the number of joints " + "(={})".format(bounds.shape[1], len(self.joints))) + return bounds + + +class JointPositionCondition(JointCondition): + r"""Joint position terminal condition + + This terminal condition describes 8 cases: + + 1. all the joint positions are: + 1. in a certain bounds and must stay between these bounds. Once one gets out, the terminal condition is over, + and results in a failure. (all=True, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once they all get out, the terminal condition is over, + and results in a success. (all=True, out=False, stay=False) + 3. outside a certain bounds and must get in. Once they all get in, the terminal condition is over, and results + in a success. (all=True, out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once one gets in, the terminal condition is over, + and results in a failure. (all=True, out=True, stay=True) + 2. any of the joint positions is: + 1. in a certain bounds and must stay between these bounds. Once they all get out, the terminal condition is + over, and results in a failure. (all=False, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once one gets out, the terminal condition is over, + and results in a success. (all=False, out=False, stay=False) + 3. outside a certain bounds and must get in. Once one gets in, the terminal condition is over, and results in + a success. (all=False ,out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once they all get in, the terminal condition is + over, and results in a failure. (all=False, out=True, stay=True) + """ + + def __init__(self, robot, joint_ids=None, bounds=(None, None), out=False, stay=False, all=False): + """ + Initialize the joint position terminal condition. + + Args: + robot (Robot): robot instance + joint_ids (int, int[N], None): joint id or list of joint ids + bounds (tuple of float / np.array[N]): bounds to stay in/out or reach/leave. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the joint positions + leave the bounds it results in a failure. if :attr:`stay` is False, it must get outside these bounds; + if the joint positions leave the bounds, it results in a success. + all (bool): this is only used if they are multiple joints. if True, all the joints are checked such that + they are inside or outside the bounds depending on the other parameters. if False, any joints will be + checked. + """ + super(JointPositionCondition, self).__init__(robot, joint_ids=joint_ids, bounds=bounds, out=out, stay=stay, + all=all) + + def _get_states(self): + """Get the joint position states.""" + return self.robot.get_joint_positions(self.joints) + + +class JointVelocityCondition(JointCondition): + r"""Joint velocity terminal condition + + This terminal condition describes 8 cases: + + 1. all the joint velocities are: + 1. in a certain bounds and must stay between these bounds. Once one gets out, the terminal condition is over, + and results in a failure. (all=True, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once they all get out, the terminal condition is over, + and results in a success. (all=True, out=False, stay=False) + 3. outside a certain bounds and must get in. Once they all get in, the terminal condition is over, and results + in a success. (all=True, out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once one gets in, the terminal condition is over, + and results in a failure. (all=True, out=True, stay=True) + 2. any of the joint velocities is: + 1. in a certain bounds and must stay between these bounds. Once they all get out, the terminal condition is + over, and results in a failure. (all=False, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once one gets out, the terminal condition is over, + and results in a success. (all=False, out=False, stay=False) + 3. outside a certain bounds and must get in. Once one gets in, the terminal condition is over, and results in + a success. (all=False ,out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once they all get in, the terminal condition is + over, and results in a failure. (all=False, out=True, stay=True) + """ + + def __init__(self, robot, joint_ids=None, bounds=(None, None), out=False, stay=False, all=False): + """ + Initialize the joint velocity terminal condition. + + Args: + robot (Robot): robot instance + joint_ids (int, int[N], None): joint id or list of joint ids + bounds (tuple of float / np.array[N]): bounds to stay in/out or reach/leave. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the joint velocities + leave the bounds it results in a failure. if :attr:`stay` is False, it must get outside these bounds; + if the joint velocities leave the bounds, it results in a success. + all (bool): this is only used if they are multiple joints. if True, all the joints are checked such that + they are inside or outside the bounds depending on the other parameters. if False, any joints will be + checked. + """ + super(JointVelocityCondition, self).__init__(robot, joint_ids=joint_ids, bounds=bounds, out=out, stay=stay, + all=all) + + def _get_states(self): + """Get the joint position states.""" + return self.robot.get_joint_velocities(self.joints) + + +class JointAccelerationCondition(JointCondition): + r"""Joint acceleration terminal condition + + This terminal condition describes 8 cases: + + 1. all the joint accelerations are: + 1. in a certain bounds and must stay between these bounds. Once one gets out, the terminal condition is over, + and results in a failure. (all=True, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once they all get out, the terminal condition is over, + and results in a success. (all=True, out=False, stay=False) + 3. outside a certain bounds and must get in. Once they all get in, the terminal condition is over, and results + in a success. (all=True, out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once one gets in, the terminal condition is over, + and results in a failure. (all=True, out=True, stay=True) + 2. any of the joint accelerations is: + 1. in a certain bounds and must stay between these bounds. Once they all get out, the terminal condition is + over, and results in a failure. (all=False, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once one gets out, the terminal condition is over, + and results in a success. (all=False, out=False, stay=False) + 3. outside a certain bounds and must get in. Once one gets in, the terminal condition is over, and results in + a success. (all=False ,out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once they all get in, the terminal condition is + over, and results in a failure. (all=False, out=True, stay=True) + """ + + def __init__(self, robot, joint_ids=None, bounds=(None, None), out=False, stay=False, all=False): + """ + Initialize the joint acceleration terminal condition. + + Args: + robot (Robot): robot instance + joint_ids (int, int[N], None): joint id or list of joint ids + bounds (tuple of float / np.array[N]): bounds to stay in/out or reach/leave. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the joint + accelerations leave the bounds it results in a failure. if :attr:`stay` is False, it must get outside + these bounds; if the joint accelerations leave the bounds, it results in a success. + all (bool): this is only used if they are multiple joints. if True, all the joints are checked such that + they are inside or outside the bounds depending on the other parameters. if False, any joints will be + checked. + """ + super(JointAccelerationCondition, self).__init__(robot, joint_ids=joint_ids, bounds=bounds, out=out, stay=stay, + all=all) + + def _get_states(self): + """Get the joint position states.""" + return self.robot.get_joint_accelerations(self.joints) + + +class JointTorqueCondition(JointCondition): + r"""Joint torque terminal condition + + This terminal condition describes 8 cases: + + 1. all the joint torques are: + 1. in a certain bounds and must stay between these bounds. Once one gets out, the terminal condition is over, + and results in a failure. (all=True, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once they all get out, the terminal condition is over, + and results in a success. (all=True, out=False, stay=False) + 3. outside a certain bounds and must get in. Once they all get in, the terminal condition is over, and results + in a success. (all=True, out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once one gets in, the terminal condition is over, + and results in a failure. (all=True, out=True, stay=True) + 2. any of the joint torques is: + 1. in a certain bounds and must stay between these bounds. Once they all get out, the terminal condition is + over, and results in a failure. (all=False, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once one gets out, the terminal condition is over, + and results in a success. (all=False, out=False, stay=False) + 3. outside a certain bounds and must get in. Once one gets in, the terminal condition is over, and results in + a success. (all=False ,out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once they all get in, the terminal condition is + over, and results in a failure. (all=False, out=True, stay=True) + """ + + def __init__(self, robot, joint_ids=None, bounds=(None, None), out=False, stay=False, all=False): + """ + Initialize the joint torque terminal condition. + + Args: + robot (Robot): robot instance + joint_ids (int, int[N], None): joint id or list of joint ids + bounds (tuple of float / np.array[N]): bounds to stay in/out or reach/leave. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the joint torques + leave the bounds it results in a failure. if :attr:`stay` is False, it must get outside these bounds; + if the joint positions leave the bounds, it results in a success. + all (bool): this is only used if they are multiple joints. if True, all the joints are checked such that + they are inside or outside the bounds depending on the other parameters. if False, any joints will be + checked. + """ + super(JointTorqueCondition, self).__init__(robot, joint_ids=joint_ids, bounds=bounds, out=out, stay=stay, + all=all) + + def _get_states(self): + """Get the joint position states.""" + return self.robot.get_joint_torques(self.joints) diff --git a/pyrobolearn/terminal_conditions/link_conditions.py b/pyrobolearn/terminal_conditions/link_conditions.py new file mode 100644 index 0000000..433477b --- /dev/null +++ b/pyrobolearn/terminal_conditions/link_conditions.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python +"""Define some link terminal conditions for the environment. +""" + +import copy +import numpy as np +from abc import ABCMeta + +from pyrobolearn.terminal_conditions.robot_condition import RobotCondition +from pyrobolearn.utils.transformation import get_rpy_from_quaternion + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2019, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "GNU GPLv3" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class LinkCondition(RobotCondition): + r"""Link Terminal Condition + + This terminal condition describes 8 cases: + + 1. all the dimensions of the link state are: + 1. in a certain bounds and must stay between these bounds. Once one gets out, the terminal condition is over, + and results in a failure. (all=True, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once they all get out, the terminal condition is over, + and results in a success. (all=True, out=False, stay=False) + 3. outside a certain bounds and must get in. Once they all get in, the terminal condition is over, and results + in a success. (all=True, out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once one gets in, the terminal condition is over, + and results in a failure. (all=True, out=True, stay=True) + 2. any of the dimension of the link state is: + 1. in a certain bounds and must stay between these bounds. Once they all get out, the terminal condition is + over, and results in a failure. (all=False, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once one gets out, the terminal condition is over, + and results in a success. (all=False, out=False, stay=False) + 3. outside a certain bounds and must get in. Once one gets in, the terminal condition is over, and results in + a success. (all=False ,out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once they all get in, the terminal condition is + over, and results in a failure. (all=False, out=True, stay=True) + + link states include its position, orientation, or velocity for instance. + """ + __metaclass__ = ABCMeta + + def __init__(self, robot, link_id, wrt_link_id=None, bounds=(None, None), dim=None, out=False, stay=False, all=all): + """ + Initialize the link terminal condition. + + Args: + robot (Robot): robot instance. + link_id (int): link id. + wrt_link_id (None, int): link id wrt which the link state is based on. if None, the state will be with + respect to the world frame. If -1, it is wrt the base frame. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the link state + leaves the bounds it results in a failure. if :attr:`stay` is False, it must get outside these bounds; + if the link state leaves the bounds, it results in a success. + all (bool): this is only used if they are multiple dimensions. if True, all the dimensions of the state + are checked if they are inside or outside the bounds depending on the other parameters. if False, any + dimensions will be checked. + """ + super(LinkCondition, self).__init__(robot, bounds=bounds, dim=dim, out=out, stay=stay, all=all) + + # set link + if not isinstance(link_id, int): + raise TypeError("Expecting the given 'link_id' to be an int, but instead got: {}".format(type(link_id))) + self.link = link_id + + # set wrt_link_id + if wrt_link_id is not None and not isinstance(wrt_link_id, int): + raise TypeError("Expecting the given 'wrt_link_id' to be an int, but instead got: " + "{}".format(type(wrt_link_id))) + self.wrt_link = wrt_link_id + + # check bounds + self.bounds = self._check_bounds(bounds) + + def _check_bounds(self, bounds): + """Check the given bounds.""" + # check the type of the bounds + if not isinstance(bounds, (tuple, list, np.ndarray)): + raise TypeError("Expecting the given bounds to be a tuple/list/np.ndarray of float, instead got: " + "{}".format(type(bounds))) + + # check that the bounds have a length of 2 (i.e. lower and upper bounds) + if len(bounds) != 2: + raise ValueError("Expecting the bounds to be of length 2 (i.e. lower and upper bounds), instead got a " + "length of {}".format(len(bounds))) + + # if one of the bounds is None, raise error + if bounds[0] is None or bounds[1] is None: + raise ValueError("Expecting the bounds to not have None, but got: {}".format(bounds)) + + # reshape bounds if necessary + bounds = np.asarray(bounds).reshape(2, -1) + return bounds + + +class LinkPositionCondition(LinkCondition): + r"""Link position terminal condition + + This terminal condition describes 8 cases: + + 1. all the dimensions of the link position state are: + 1. in a certain bounds and must stay between these bounds. Once one gets out, the terminal condition is over, + and results in a failure. (all=True, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once they all get out, the terminal condition is over, + and results in a success. (all=True, out=False, stay=False) + 3. outside a certain bounds and must get in. Once they all get in, the terminal condition is over, and results + in a success. (all=True, out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once one gets in, the terminal condition is over, + and results in a failure. (all=True, out=True, stay=True) + 2. any of the dimension of the link position state is: + 1. in a certain bounds and must stay between these bounds. Once they all get out, the terminal condition is + over, and results in a failure. (all=False, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once one gets out, the terminal condition is over, + and results in a success. (all=False, out=False, stay=False) + 3. outside a certain bounds and must get in. Once one gets in, the terminal condition is over, and results in + a success. (all=False ,out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once they all get in, the terminal condition is + over, and results in a failure. (all=False, out=True, stay=True) + """ + + def __init__(self, robot, link_id=None, wrt_link_id=None, bounds=(None, None), dim=None, out=False, stay=False, + all=False): + """ + Initialize the link position terminal condition. + + Args: + robot (Robot): robot instance. + link_id (int, int[N], None): link id or list of link ids. + wrt_link_id (None, int): link id wrt which the position is based on. if None, the position will be with + respect to the world frame. If -1, it is wrt the base frame. + bounds (tuple of 2 np.array[3] / np.array[N,3], np.array[2,3], np.array[2,N,3]): bounds to stay in/out or + reach/leave. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the link position + leaves the bounds it results in a failure. if :attr:`stay` is False, it must get outside these bounds; + if the link position leaves the bounds, it results in a success. + """ + super(LinkPositionCondition, self).__init__(robot, link_id=link_id, wrt_link_id=wrt_link_id, bounds=bounds, + dim=dim, out=out, stay=stay, all=all) + + def _get_states(self): + """Return the link position state.""" + # get the link position + if self.wrt_link is None: + position = self.robot.get_link_world_positions(link_ids=self.link, flatten=True) + else: + position = self.robot.get_link_positions(link_ids=self.link, wrt_link_id=self.wrt_link, flatten=True) + + if self.dim is None: + return position + return position[self.dim] + + +class LinkOrientationCondition(LinkCondition): + r"""Link orientation terminal condition + + This terminal condition describes 8 cases: + + 1. all the dimensions of the link orientation (expressed as roll-pitch-yaw angles) state are: + 1. in a certain bounds and must stay between these bounds. Once one gets out, the terminal condition is over, + and results in a failure. (all=True, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once they all get out, the terminal condition is over, + and results in a success. (all=True, out=False, stay=False) + 3. outside a certain bounds and must get in. Once they all get in, the terminal condition is over, and results + in a success. (all=True, out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once one gets in, the terminal condition is over, + and results in a failure. (all=True, out=True, stay=True) + 2. any of the dimension of the link orientation (expressed as roll-pitch-yaw angles) state is: + 1. in a certain bounds and must stay between these bounds. Once they all get out, the terminal condition is + over, and results in a failure. (all=False, out=False, stay=True) + 2. in a certain bounds and must get out of these bounds. Once one gets out, the terminal condition is over, + and results in a success. (all=False, out=False, stay=False) + 3. outside a certain bounds and must get in. Once one gets in, the terminal condition is over, and results in + a success. (all=False ,out=True, stay=False) + 4. outside a certain bounds and must stay outside these ones. Once they all get in, the terminal condition is + over, and results in a failure. (all=False, out=True, stay=True) + + Warnings: the orientation is expressed as roll-pitch-yaw angles. + """ + + def __init__(self, robot, link_id, wrt_link_id=None, bounds=(None, None), dim=None, out=False, stay=False, + all=False): + """ + Initialize the link orientation terminal condition. + + Args: + robot (Robot): robot instance. + link_id (int, None): link id or list of link ids. + wrt_link_id (None, int): link id wrt which the orientation is based on. if None, the orientation will be + with respect to the world frame. If -1, it is wrt the base frame. + bounds (tuple of 2 float / np.array[3], np.array[2], np.array[2,3]): bounds to stay in/out or reach/leave. + the orientation is expressed as roll-pitch-yaw angles. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the link + orientation leaves the bounds it results in a failure. if :attr:`stay` is False, it must get outside + these bounds; if the link orientation leaves the bounds, it results in a success. + """ + super(LinkOrientationCondition, self).__init__(robot, link_id=link_id, wrt_link_id=wrt_link_id, bounds=bounds, + dim=dim, out=out, stay=stay, all=all) + + def _get_states(self): + """Return the link orientation state.""" + # get the link orientation + if self.wrt_link is None: + orientation = self.robot.get_link_world_orientations(link_ids=self.link, flatten=True) + else: + orientation = self.robot.get_link_orientations(link_ids=self.link, wrt_link_id=self.wrt_link, flatten=True) + + # convert from quaternion to roll-pitch-yaw angles + orientation = get_rpy_from_quaternion(orientation) + + # return the proper orientation + if self.dim is None: + return orientation + return orientation[self.dim] + + +# class LinkVelocityCondition(LinkCondition): +# r"""Link velocity terminal condition +# """ +# raise NotImplementedError diff --git a/pyrobolearn/terminal_conditions/robot_condition.py b/pyrobolearn/terminal_conditions/robot_condition.py new file mode 100644 index 0000000..12fea96 --- /dev/null +++ b/pyrobolearn/terminal_conditions/robot_condition.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +"""Define some robot terminal conditions for the environment. +""" + +from abc import ABCMeta + +from pyrobolearn.robots.robot import Robot +from pyrobolearn.terminal_conditions.body_conditions import BodyCondition + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2019, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "GNU GPLv3" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class RobotCondition(BodyCondition): + r"""Robot Terminal Condition + + """ + __metaclass__ = ABCMeta + + def __init__(self, robot, bounds=(None, None), dim=None, out=False, stay=False, all=False): + """ + Initialize the robot terminal condition. + + Args: + robot (Robot): robot instance + dim (None, int, int[3]): dimensions that we should consider when looking at the bounds. If None, it will + consider all 3 dimensions. If one dimension is provided it will only check along that dimension. If + a np.array of 0 and 1 is provided, it will consider the dimensions that are equal to 1. Thus, [1,0,1] + means to consider the bounds along the x and z axes. + out (bool): if True, we are outside the provided bounds. If False, we are inside the provided bounds. + stay (bool): if True, it must stay in the bounds defined by in_bounds or out_bounds; if the state + leave the bounds it results in a failure. if :attr:`stay` is False, it must get outside these bounds; + if the state leaves the bounds, it results in a success. + all (bool): this is only used if they are multiple dimensions. if True, all the dimensions of the state + are checked if they are inside or outside the bounds depending on the other parameters. if False, any + dimensions will be checked. + """ + super(RobotCondition, self).__init__(robot, bounds=bounds, dim=dim, out=out, stay=stay, all=all) + self.robot = robot + + @property + def robot(self): + """Return the robot instance.""" + return self._robot + + @robot.setter + def robot(self, robot): + """Set the robot instance.""" + if not isinstance(robot, Robot): + raise TypeError("Expecting the given 'robot' to be an instance of `Robot`, instead got: " + "{}".format(type(robot))) + self._robot = robot diff --git a/pyrobolearn/terminal_conditions/terminal_condition.py b/pyrobolearn/terminal_conditions/terminal_condition.py index c40157e..0a3334c 100644 --- a/pyrobolearn/terminal_conditions/terminal_condition.py +++ b/pyrobolearn/terminal_conditions/terminal_condition.py @@ -35,55 +35,128 @@ class TerminalCondition(object): """ # TODO: we should be able to combine different conditions using 'OR' and 'AND' + def __init__(self, btype=None, name=None): + """ + Initialize the terminal condition. + + Args: + btype (bool, str, None): if the terminal condition represents a failure or success condition. If None, it + represents a neutral terminal condition (which is neither a failure or success condition, but just + means the episode is over). If string, it has to be among {"success", "failure", "neutral"}. + name (str): name of the final condition + """ + self.btype = btype + self._over = False + self._achieved = False + self.name = name + + ############## + # Properties # + ############## + + @property + def btype(self): + """Return if the terminal condition is a neutral (None), a failure (False) or a success (True) condition.""" + return self._btype + + @btype.setter + def btype(self, value): + """Set the success variable.""" + if value is not None and not isinstance(value, bool) and not isinstance(value, str): + raise TypeError("Expecting the given 'btype' variable to be a boolean, string, or None, but got instead: " + "{}".format(type(value))) + if isinstance(value, str): + if value == 'neutral': + value = None + elif value == 'failure': + value = False + elif value == 'success': + value = True + else: + raise ValueError("Expecting the given string to be among ['success', 'failure', 'neutral'], but " + "instead got: {}".format(value)) + self._btype = value + + ########### + # Methods # + ########### + + def is_over(self): + """Return if the condition is over or not.""" + return self._over + + def succeeded(self): + """Return if the condition succeeded or not.""" + return self._achieved + + def type(self): + """Return if it a success, failure, or neutral terminal condition.""" + return self._btype + + def type_str(self): + """Return the string representing the type of terminal condition.""" + if self._btype is None: + return "neutral" + if self._btype: + return "success" + return "failure" + + def reset(self): + """ + Reset the terminal condition. + """ + self._over = False + def check(self): """ Check if the terminating condition has been fulfilled, and return True or False accordingly """ - return False + return self._over + + ############# + # Operators # + ############# def __str__(self): + """Return a string describing the terminal condition.""" return self.__class__.__name__ - def __call__(self, *args, **kwargs): + def __call__(self): + """Check the terminal condition.""" return self.check() def __bool__(self): + """Return a bool based on the terminating condition.""" return self.check() __nonzero__ = __bool__ -class FailedCondition(TerminalCondition): - r"""Failed Terminal Condition - - This determines when a policy or multiple ones have failed to perform a certain task. - """ - pass +# class FailedCondition(TerminalCondition): +# r"""Failed Terminal Condition +# +# This determines when a policy or multiple ones have failed to perform a certain task. +# """ +# pass +# +# +# class NeutralCondition(TerminalCondition): +# r"""Neutral Terminal Condition +# +# This determines when an environment has ended. +# """ +# pass +# +# +# class SucceededCondition(TerminalCondition): +# r"""Succeeded Terminal Condition +# +# This determines when a policy or multiple ones have succeeded to perform a certain task. +# """ +# pass -class SucceededCondition(TerminalCondition): - r"""Succeeded Terminal Condition - - This determines when a policy or multiple ones have succeeded to perform a certain task. - """ - pass - - -class GymTerminalCondition(TerminalCondition): - r"""OpenAI Gym Terminal Condition - - Returns if the OpenAI Gym environment has terminated. This does not provide any information if the environment - terminated because the policy succeeded or failed to perform the task. - """ - - def __init__(self, done=False): - self.done = done - - def check(self): - return self.done - - -class HasFallen(FailedCondition): +class HasFallenCondition(TerminalCondition): r"""Has Fallen Condition Check if the given robot has fallen, by checking if its base is below a certain threshold. @@ -103,6 +176,7 @@ class HasFallen(FailedCondition): robot has fallen. Normally, the initial robot base up vector points upward. By default, it is 30 degrees (=pi/6 rad). """ + super(HasFallenCondition, self).__init__(btype='failure') self.robot = robot self.height_threshold = height_threshold if height_threshold is not None else robot.base_height/3. self.angle_threshold = angle_threshold @@ -125,6 +199,7 @@ class HasFallen(FailedCondition): return height_condition or angle_condition def __str__(self): + """Return a string describing the terminal condition.""" description = '{} (\n\tbase_height={} ?? angle_threshold={}' \ '\n)'.format(self.__class__.__name__, self._compute_height(), self.height_threshold, self._compute_angle(), self.angle_threshold) @@ -150,16 +225,18 @@ class HasFallen(FailedCondition): return terminal -class HasReached(SucceededCondition): +class HasReachedCondition(TerminalCondition): r"""Has Reached Condition Check if the robot or a part of it has reached a certain position, configuration, or state for a certain amount of time/steps. """ - pass + + def __init__(self): + super(HasReachedCondition, self).__init__(btype='success') -class LinkInSpecifiedDirection(HasReached): +class LinkInSpecifiedDirection(HasReachedCondition): r"""Check if the specified link is in certain direction for a certain amount of time steps. Specifically, it checks if the position of the link with respect to the world or another link is in a certain @@ -168,6 +245,7 @@ class LinkInSpecifiedDirection(HasReached): """ def __init__(self, state, direction, domain=(0.95, 1.), total_steps=0): + super(LinkInSpecifiedDirection, self).__init__() if not isinstance(state, LinkState): raise TypeError("Expecting the state to be an instance of LinkState, instead got: {}".format(type(state))) self.state = state @@ -209,6 +287,7 @@ class LinkInSpecifiedDirection(HasReached): return False def __str__(self): + """Return a string describing the terminal condition.""" return self.__class__.__name__ + '(direction=' + str(self.direction) + ')' def __copy__(self):