fix bugs + update sensors/actuators

This commit is contained in:
Brian Delhaisse
2019-07-21 23:09:05 +02:00
parent 740701ed7c
commit 6dff906456
31 changed files with 2062 additions and 290 deletions
+6
View File
@@ -3,6 +3,7 @@
name = "pyrobolearn"
import os
import sys
import signal
from itertools import count
@@ -121,6 +122,11 @@ def module_imported(module_name): # TODO: improve this method
return False
world_mesh_path = os.path.dirname(os.path.abspath(__file__)) + '/worlds/meshes/'
__all__ = [simulators, robots, worlds, physics, states, actions, terminal_conditions, rewards, envs, models,
approximators, policies, values, actorcritics, dynamics, tools]
# Define what submodules/classes/functions should be loaded when writing 'from pyrobolearn import *'
# __all__ = [
# # Submodules
+36 -4
View File
@@ -7,7 +7,7 @@ other joint actuators. Additionally, this is important as more realistic motors
simulation to reality.
"""
# TODO: add latency + noise
from pyrobolearn.robots.noise.noise import Noise, NoNoise
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
@@ -26,20 +26,42 @@ class Actuator(object):
Other actuators such as speakers, leds, and others are attached to links.
"""
def __init__(self, latency=0):
def __init__(self, noise=None, latency=0):
"""
Initialize the actuator.
Args:
latency (int, float): latency.
noise (None, Noise): noise to be added.
latency (int, float, None): latency time / step.
"""
# variable to check if the actuator is enabled
self._enabled = True
# set the latency
if not isinstance(latency, (int, float)):
raise TypeError("Expecting the given 'latency' to be an int or float, instead got: "
"{}".format(type(latency)))
if latency < 0:
raise ValueError("Expecting the given 'latency' to be a positive number, but got instead: "
"{}".format(latency))
self._latency = latency
self._latent_cnt = -1
# set the noise
if noise is None:
noise = NoNoise()
if not isinstance(noise, Noise):
raise TypeError("Expecting the given 'noise' to be an instance of Noise, instead got: "
"{}".format(type(noise)))
self._noise = noise
# self.sim = simulator
#
##############
# Properties #
##############
# @property
# def simulator(self):
# return self.sim
@@ -48,6 +70,16 @@ class Actuator(object):
# def simulator(self, simulator):
# self.sim = simulator
@property
def enabled(self):
"""Return if the sensor is enabled or not."""
return self._enabled
@property
def disabled(self):
"""Return if the sensor is disabled or not."""
return not self._enabled
###########
# Methods #
###########
+24 -5
View File
@@ -4,6 +4,7 @@
import copy
import numpy as np
from abc import ABCMeta
from pyrobolearn.robots.actuators.actuator import Actuator
@@ -18,16 +19,24 @@ __status__ = "Development"
class JointActuator(Actuator):
r"""Joint Actuators
r"""Joint Actuators (abstract)
This defined the joint actuator class; this is an actuator which is attached to a joint and outputs the torque
to be applied on it using a specific control scheme (e.g. PD control).
For instance, given a target joint position value, the actuator computes the necessary torque to be applied on
the joint using a simple PD control (with certain gains).
"""
__metaclass__ = ABCMeta
def __init__(self, joint_id):
super(JointActuator, self).__init__()
def __init__(self, joint_id, latency=None):
"""
Initialize the joint actuator.
Args:
joint_id (int): joint unique id.
latency (int, float, None): latency time / step.
"""
super(JointActuator, self).__init__(latency=latency)
self.joint_id = joint_id
def __copy__(self):
@@ -55,12 +64,22 @@ class PDJointActuator(JointActuator):
"""
def __init__(self, joint_id, kp=0, kd=0, min_torque=-np.infty, max_torque=np.infty, latency=0):
super(PDJointActuator, self).__init__(joint_id)
"""
Initialize the PD joint actuator.
Args:
joint_id (int): joint id.
kp (float): position gain
kd (float): velocity gain
min_torque (float): minimum torque
max_torque (float): maximum torque
latency (int, float, None): latency time / step.
"""
super(PDJointActuator, self).__init__(joint_id, latency=latency)
self.kp = kp
self.kd = kd
self.min_torque = min_torque
self.max_torque = max_torque
self.latency = latency
def compute(self, qd, q, dq):
"""
@@ -0,0 +1,85 @@
#!/usr/bin/env python
"""Define the Gaussian noise class.
This noise can be applied notably on sensors and actuators.
"""
import numpy as np
from pyrobolearn.robots.noise.noise import Noise
__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 GaussianNoise(Noise):
r"""Gaussian noise class
This computes:
.. math:: y = x + \epsilon
where :math:`x` is the original data, :math:`y` is the noisy data, and
:math:`\epsilon \sim \mathcal{N}(\mu, \Sigma)` is the added Gaussian noise.
"""
def __init__(self, vars=1):
"""Initialize the noise.
Args:
vars (float, int, np.array): variances or covariance matrix.
"""
super(GaussianNoise, self).__init__()
self._multivariate = True if isinstance(vars, np.ndarray) and vars.ndim == 2 else False
if self._multivariate:
mean = np.zeros(vars.shape[0])
else:
if not isinstance(vars, (int, float, np.ndarray)):
raise TypeError("Expecting the given 'vars' to be an int, float, or np.array, instead got: "
"{}".format(type(vars)))
if isinstance(vars, np.ndarray) and vars.ndim > 1:
raise ValueError("Expecting the given 'vars' to be a np.array of dim 1 or 2, instead got: "
"{}".format(vars.ndim))
vars = np.sqrt(vars) # abuse of name; this is no more the variance but the standard deviation
mean = 0.
self.vars = vars
self.mean = mean
def apply_noise(self, data, inplace=False):
"""
Apply the noise on the given data.
Args:
data (int, float, np.array): data to apply the noise on.
inplace (bool): if True, it will directly modify the given data, and won't return a copy of it.
Returns:
int, float, np.array: noisy data.
"""
# compute noise
if self._multivariate:
if isinstance(data, np.ndarray) and data.shape != self.vars.shape:
noise = np.random.multivariate_normal(mean=self.mean, cov=self.vars, size=data.shape)
else:
noise = np.random.multivariate_normal(mean=self.mean, cov=self.vars)
else:
if isinstance(data, np.ndarray):
noise = np.random.normal(loc=self.mean, scale=self.vars, size=data.shape)
else:
noise = np.random.normal(loc=self.mean, scale=self.vars)
# check if inplace operation
if not inplace:
return data + noise
data += noise
return data
def __str__(self):
"""Return a string describing the class."""
return self.__class__.__name__ + "(mean=0, vars=" + str(self.vars) + ")"
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python
"""Define the abstract noise class from which all noises inherit from.
The noise can be applied notably on sensors and actuators.
"""
from abc import ABCMeta
__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 Noise(object):
r"""Noise (abstract) class
Abstract Noise class.
"""
__metaclass__ = ABCMeta
def __init__(self):
"""Initialize the noise.
"""
pass
def apply_noise(self, data, inplace=False):
"""
Apply the noise on the given data.
Args:
data (int, float, np.array): data to apply the noise on.
inplace (bool): if True, it will directly modify the given data, and won't return a copy of it.
Returns:
int, float, np.array: noisy data.
"""
raise NotImplementedError
def __str__(self):
"""Return a string describing the class."""
return self.__class__.__name__
def __call__(self, data, inplace=False):
"""Apply the noise on the given data."""
return self.apply_noise(data, inplace=inplace)
class NoNoise(Noise):
r"""No noise class.
This is a dummy class that don't apply any noises on the given data and just return it.
"""
def apply_noise(self, data, inplace=False):
"""
Apply the noise on the given data.
Args:
data (int, float, np.array): data to apply the noise on.
inplace (bool): if True, it will directly modify the given data, and won't return a copy of it.
Returns:
int, float, np.array: noisy data.
"""
return data
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python
"""Define the Uniform noise class.
This noise can be applied notably on sensors and actuators.
"""
import numpy as np
from pyrobolearn.robots.noise.noise import Noise
__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 UniformNoise(Noise):
r"""Uniform noise class
This computes:
.. math:: y = x + \epsilon
where :math:`x` is the original data, :math:`y` is the noisy data, and
:math:`\epsilon \sim \mathcal{U}(a=-\frac{w}{2}, b=\frac{w}{2})` is the added Uniform noise where :math:`w = b-a`
is the width of the distribution.
"""
def __init__(self, width=1.):
"""Initialize the noise.
Args:
width (float, int, np.array): variances or covariance matrix.
"""
super(UniformNoise, self).__init__()
self.width = width
self.radius = self.width/2.
def apply_noise(self, data, inplace=False):
"""
Apply the noise on the given data.
Args:
data (int, float, np.array): data to apply the noise on.
inplace (bool): if True, it will directly modify the given data, and won't return a copy of it.
Returns:
int, float, np.array: noisy data.
"""
if isinstance(data, np.ndarray):
noise = np.random.uniform(low=-self.radius, high=self.radius, size=data.shape)
else:
noise = np.random.uniform(low=-self.radius, high=self.radius)
# check if inplace operation
if not inplace:
return data + noise
data += noise
return data
def __str__(self):
"""Return a string describing the class."""
return self.__class__.__name__ + "(a=" + str(-self.radius) + "b=" + str(self.radius) + ")"
+3 -3
View File
@@ -64,16 +64,16 @@ class Pepper(WheeledRobot, BiManipulator):
# Note that we divide width and height by 4 (otherwise the simulator is pretty slow)
self.camera_top = CameraSensor(self.sim, self.id, 4, width=2560 / 4, height=1080 / 4, fovy=44.30,
near=0.3, far=100, rate=60)
near=0.3, far=100, ticks=60)
self.camera_bottom = CameraSensor(self.sim, self.id, 9, width=2560 / 4, height=1080 / 4, fovy=44.30,
near=0.3, far=100, rate=60)
near=0.3, far=100, ticks=60)
# 3D camera sensor
# From [1]: "One 3D camera is located in the forehead. It provides image resolution up to 320x240 at
# 20 frames per second. One ASUS Xtion 3D sensor is located behind the eyes. VFOV = 45 deg, HFOV = 58 deg,
# focus = [80cm, 3.5m]."
self.camera_depth = CameraSensor(self.sim, self.id, 6, width=320, height=240, fovy=45, near=0.3, far=3.5,
rate=120)
ticks=120)
self.cameras = [self.camera_top, self.camera_bottom, self.camera_depth]
+194 -27
View File
@@ -22,6 +22,8 @@ from pyrobolearn.utils.transformation import *
from pyrobolearn.utils.manifold_utils import tensor_matrix_product, symmetric_matrix_to_vector, logarithm_map, \
distance_spd
from pyrobolearn.robots.base import ControllableBody
from pyrobolearn.robots.sensors.sensor import Sensor
from pyrobolearn.robots.actuators.actuator import Actuator
__author__ = "Brian Delhaisse"
@@ -167,8 +169,8 @@ class Robot(ControllableBody):
self.kp, self.kd = None, None
# sensors and actuators
self.sensors = [] # list of sensors
self.actuators = [] # list of actuators
self.sensors = {} # dict of sensors {SensorClass: [sensorInstance]}
self.actuators = {} # dict of actuators {ActuatorClass: [actuatorInstance]}
#############
# Operators #
@@ -227,6 +229,60 @@ class Robot(ControllableBody):
"""Return the number of joints that are not fixed."""
return len(self.joints)
@property
def sensors(self):
"""Return the dict of Sensor instances."""
return self._sensors
@sensors.setter
def sensors(self, sensors):
"""Set the dict of Sensor instances."""
if sensors is None:
sensors = {}
elif isinstance(sensors, Sensor):
sensors = {sensors.__class__: [sensors]}
elif isinstance(sensors, (list, tuple, dict)):
if isinstance(sensors, dict):
sensors = sensors.values()
sensor_dict = {}
for i, sensor in enumerate(sensors):
if not isinstance(sensor, Sensor):
raise TypeError("Expecting the {}th sensor to be an instance of `Sensor`, instead got: "
"{}".format(i, type(sensor)))
sensor_dict.setdefault(sensor.__class__, []).append(sensor)
sensors = sensor_dict
else:
raise TypeError("Expecting the given 'sensors' to be a `Sensor` or a list of `Sensor`, instead got: "
"{}".format(type(sensors)))
self._sensors = sensors
@property
def actuators(self):
"""Return the dict of Actuator instances."""
return self._actuators
@actuators.setter
def actuators(self, actuators):
"""Set the dict of Actuator instances."""
if actuators is None:
actuators = {}
elif isinstance(actuators, Actuator):
actuators = {actuators.__class__: [actuators]}
elif isinstance(actuators, (list, tuple, dict)):
if isinstance(actuators, dict):
actuators = actuators.values()
actuator_dict = {}
for i, actuator in enumerate(actuators):
if not isinstance(actuator, Actuator):
raise TypeError("Expecting the {}th actuator to be an instance of `Actuator`, instead got: "
"{}".format(i, type(actuator)))
actuator_dict.setdefault(actuator.__class__, []).append(actuator)
actuators = actuator_dict
else:
raise TypeError("Expecting the given 'actuators' to be a `Actuator` or a list of `Actuator`, instead got: "
"{}".format(type(actuators)))
self._actuators = actuators
###########
# Methods #
###########
@@ -244,13 +300,16 @@ class Robot(ControllableBody):
def sense(self):
"""Run all the sensors."""
for sensor in self.sensors:
sensor()
for sensors in self.sensors.itervalues():
for sensor in sensors:
sensor.clean()
sensor.sense()
def act(self):
"""Run all the actuators."""
for actuator in self.actuators:
actuator()
for actuators in self.actuators.itervalues():
for actuator in actuators:
actuator()
########
# Base #
@@ -402,6 +461,28 @@ class Robot(ControllableBody):
return np.concatenate((acc[0], acc[1]))
return acc
def get_base_linear_acceleration(self):
"""
Return the linear acceleration of the base. Some simulators does not provide the accelerations.
If that is the case, then we use finite difference to compute it (calling this the first time will return a
zero vector for the linear acceleration).
Returns:
np.array[3]: linear acceleration
"""
return self.get_base_acceleration(concatenate=False)[0]
def get_base_angular_acceleration(self):
"""
Return the angular acceleration of the base. Some simulators does not provide the accelerations.
If that is the case, then we use finite difference to compute it (calling this the first time will return a
zero vector for the angular acceleration).
Returns:
np.array[3]: angular acceleration
"""
return self.get_base_acceleration(concatenate=False)[1]
def get_base_spatial_acceleration(self):
"""
Return the base spatial acceleration (which is the concatenation of the angular and linear acceleration).
@@ -1224,6 +1305,12 @@ class Robot(ControllableBody):
r"""
Reset the state of the robot.
Args:
q (int, float, np.array[N], None): joint position(s).
dq (int, float, np.array[N], None): joint velocity(ies).
joint_ids (int, int[N], None): joint id, or list of joint ids. If None, it will disable the motors of
all actuated joints.
Warnings: This is only valid in the simulator, and note that calling this method overrides all physics
simulation.
"""
@@ -1610,10 +1697,10 @@ class Robot(ControllableBody):
"""
# check if cached
if 'link_pos' in self._state:
pos = self._state['link_pos'][0] # (N,6)
pos = self._state['link_pos'][0] # (N,3)
else:
links = list(range(self.num_links))
pos = self.sim.get_link_world_positions(body_id=self.id, link_ids=links) # (N,6)
pos = self.sim.get_link_world_positions(body_id=self.id, link_ids=links) # (N,3)
self._state['link_pos'] = [pos, time.time()]
# if one link
@@ -1651,6 +1738,10 @@ class Robot(ControllableBody):
p1 = self.get_link_world_positions(link_ids, flatten=False)
p0 = self.get_base_position() if wrt_link_id is None or wrt_link_id == -1 \
else self.get_link_world_positions(wrt_link_id, flatten=False)
# p^0 = o^0_1 + R^0_1 p^1
# Notation: ^i = expressed in frame i, _j = point j, o^i_j = origin position of point j in frame i
# TODO: should I express it in the world coordinate frame or in the wrt_link frame??
p = (p1 - p0)
if flatten:
return p.reshape(-1)
@@ -1706,7 +1797,9 @@ class Robot(ControllableBody):
q0 = np.asarray([get_quaternion_inverse(self.get_link_world_orientations(link))
for link in wrt_link_id])
# R^w_1 = R^w_0 R^0_1 <--> R^0_1 = (R^w_0)^{-1} R^w_1 = R^0_w R^w_1 --> q = q_0^{-1} * q_1
q = get_quaternion_product(q0, q1)
if flatten:
q.reshape(-1)
return q
@@ -2065,7 +2158,57 @@ class Robot(ControllableBody):
return acc.reshape(-1) # (N*6,)
return acc
def get_spatial_link_world_acceleration(self, link_ids=None, flatten=True):
def get_link_world_linear_accelerations(self, link_ids=None, flatten=True):
"""
Return the linear accelerations (expressed in the Cartesian world space coordinates) for the given link(s).
See :func:`~get_link_world_accelerations` for more information.
Args:
link_ids (int, int[N], None): link id, or list of desired link ids. If None, get the linear
accelerations of all links associated to actuated joints.
flatten (bool): if True, it will return a 1D array instead of a 2D array
Returns:
if 1 link:
np.array[3]: linear acceleration of the link in the Cartesian world space
if multiple links:
np.array[N*3], np.array[N,3]: linear acceleration of each link
"""
accelerations = self.get_link_world_accelerations(link_ids=link_ids, flatten=False)
if isinstance(link_ids, int):
return accelerations[:3]
accelerations = accelerations[:, :3]
if flatten:
return accelerations.reshape(-1)
return accelerations
def get_link_world_angular_accelerations(self, link_ids=None, flatten=True):
"""
Return the angular accelerations (expressed in the Cartesian world space coordinates) for the given link(s).
See :func:`~get_link_world_accelerations` for more information.
Args:
link_ids (int, int[N], None): link id, or list of desired link ids. If None, get the angular
accelerations of all links associated to actuated joints.
flatten (bool): if True, it will return a 1D array instead of a 2D array
Returns:
if 1 link:
np.array[3]: angular acceleration of the link in the Cartesian world space
if multiple links:
np.array[N*3], np.array[N,3]: angular acceleration of each link
"""
accelerations = self.get_link_world_accelerations(link_ids=link_ids, flatten=False)
if isinstance(link_ids, int):
return accelerations[3:]
accelerations = accelerations[:, 3:]
if flatten:
return accelerations.reshape(-1)
return accelerations
def get_spatial_link_world_accelerations(self, link_ids=None, flatten=True):
r"""
Return the spatial link world accelerations which is the concatenation of the angular and linear accelerations.
The difference with :func:`~get_link_world_accelerations` is that this one returns the concatenation of the
@@ -4038,26 +4181,38 @@ class Robot(ControllableBody):
return self.sensors
return self.sensors[idx]
def get_imu(self, idx=0):
pass
def add_sensor(self, sensor):
"""
Add a sensor to the list of sensors.
def get_force_torque_sensor(self, idx=0):
pass
Args:
sensor (Sensor): sensor instance.
"""
if not isinstance(sensor, Sensor):
raise TypeError("Expecting the given 'sensor' to be an instance of `Sensor`, instead got: "
"{}".format(sensor))
self.sensors.setdefault(sensor.__class__, []).append(sensor)
def has_camera(self):
return False
def get_camera(self, idx=0):
pass
def get_camera_image(self, idx=0):
pass
def get_main_camera(self):
pass
def get_main_camera_image(self):
pass
# def get_imu(self, idx=0):
# pass
#
# def get_force_torque_sensor(self, idx=0):
# pass
#
# def has_camera(self):
# return False
#
# def get_camera(self, idx=0):
# pass
#
# def get_camera_image(self, idx=0):
# pass
#
# def get_main_camera(self):
# pass
#
# def get_main_camera_image(self):
# pass
#############
# Actuators #
@@ -4087,6 +4242,18 @@ class Robot(ControllableBody):
return self.actuators
return self.actuators[idx]
def add_actuator(self, actuator):
"""
Add an actuator to the list of actuators.
Args:
actuator (Actuator): actuator instance.
"""
if not isinstance(actuator, Actuator):
raise TypeError("Expecting the given 'actuator' to be an instance of `Actuator`, instead got: "
"{}".format(actuator))
self.actuators.setdefault(actuator.__class__, []).append(actuator)
#########
# Debug #
#########
+13 -7
View File
@@ -3,22 +3,28 @@
from .sensor import Sensor
# import joint + encoder sensors
from .joints import *
from .joints import JointSensor, JointEncoderSensor
# import link sensors
from .links import *
from .links import LinkSensor
# import ft sensors
from .force_torque import *
from .force_torque import JointTorqueSensor, JointForceTorqueSensor
# import imu sensors
from .imu import *
from .imu import IMUSensor
# import contact sensors
from .contact import ContactSensor
# import camera sensors
from .camera import *
from .camera import CameraSensor, DepthCameraSensor, RGBCameraSensor
# import rays
from .ray import RaySensor, RayBatchSensor, HeightmapSensor
# import light / laser sensors
from .light import *
# from .light import *
# import miscellaneous sensors
from .misc import *
# from .misc import *
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python
"""Define the altimeter sensor which measures the altitude of an object.
"""
import numpy as np
from pyrobolearn.robots.sensors.links import LinkSensor
__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 AltimeterSensor(LinkSensor):
r"""Altimeter sensor
'An altimeter or an altitude meter is an instrument used to measure the altitude of an object above a fixed level.'
[1] Here the fixed level is set to be the ground.
References:
- [1] Altimeter (Wikipedia): https://en.wikipedia.org/wiki/Altimeter
"""
def __init__(self, simulator, body_id, link_id=-1, noise=None, ticks=1, latency=None, position=None,
orientation=None):
"""
Initialize the altimeter sensor.
Args:
simulator (Simulator): simulator instance.
body_id (int): unique body id.
link_id (int): unique id of the link.
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
position (np.array[3], None): local position of the sensor with respect to the given link. If None, it will
be the zero vector.
orientation (np.array[4], None): local orientation of the sensor with respect to the given link (expressed
as a quaternion [x,y,z,w]). If None, it will be the unit quaternion [0,0,0,1].
"""
super(AltimeterSensor, self).__init__(simulator, body_id=body_id, link_id=link_id, noise=noise, ticks=ticks,
latency=latency, position=position, orientation=orientation)
def _sense(self, apply_noise=True):
"""
Sense the altitude.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
float: altitude (in z-direction).
"""
# check if the simulator supports that sensor
if self.sim.supports_sensors("altimeter"):
return self.sim.get_sensor("altimeter", self.body_id, self.link_id).sense()
z = self.get_link_world_position()[-1] # get the z direction
if apply_noise:
z = self._noise(z)
return z
+90 -24
View File
@@ -19,7 +19,7 @@ __email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class CameraSensor(LinkSensor):
class CameraSensor(LinkSensor): # TODO: double-check this class
r"""Camera Sensor class.
The following operations are carried out (in the given order) to display images seen by the camera:
@@ -44,7 +44,7 @@ class CameraSensor(LinkSensor):
Examples:
sim = BulletClient(connection_mode=p.GUI)
cam = Camera(sim, width=400, height=400, target_position=(0,0,0), eyePosition=(2,0,1))
cam = Camera(sim, width=400, height=400, target_position=(0,0,0), position=(2,0,1))
img = cam.get_rgb_image()
plt.imshow(img)
plt.show()
@@ -56,7 +56,8 @@ class CameraSensor(LinkSensor):
[4] http://learnwebgl.brown37.net/08_projections/projections_perspective.html
"""
def __init__(self, simulator, body_id, link_id, width, height, position=None, orientation=None, rate=50,
def __init__(self, simulator, body_id, link_id, width, height, noise=None, ticks=50, latency=None,
position=None, orientation=None,
target_position=None, distance=10.,
fovy=60, aspect=None, near=0.01, far=100.,
left=None, right=None, bottom=None, top=None):
@@ -65,15 +66,22 @@ class CameraSensor(LinkSensor):
Args:
simulator: simulator to access to the sensor.
position (list/tuple/array of 3 floats): position of the sensor
orientation (quaternion): orientation of the sensor
body_id (int): unique id of the body.
link_id (int): unique id of the link.
width (int): width of the returned pictures
height (int): height of the returned pictures
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
position (np.array[3], None): local position of the sensor with respect to the given link. If None, it will
be the zero vector.
orientation (np.array[4], None): local orientation of the sensor with respect to the given link (expressed
as a quaternion [x,y,z,w]). If None, it will be the unit quaternion [0,0,0,1].
For a view matrix:
target_position (list/tuple of 3 floats): target focus point in cartesian world coordinates
eyePosition (list/tuple of 3 floats): eye position in cartesian world coordinates
UpVector (list/tuple of 3 floats): up vector of the camera in cartesian world coordinates
position (list/tuple of 3 floats): eye position in cartesian world coordinates
up_vector (list/tuple of 3 floats): up vector of the camera in cartesian world coordinates
For a view matrix from RPY:
target_position (list/tuple of 3 floats): target focus point in cartesian world coordinates
@@ -81,7 +89,7 @@ class CameraSensor(LinkSensor):
yaw (float): yaw angle in degrees of the camera
pitch (float): pitch angle in degrees of the camera
roll (float): roll angle in degrees of the camera
upAxisIndex (int): either 1 for Y or 2 for Z axis up (default Z axis)
up_axis_index (int): either 1 for Y or 2 for Z axis up (default Z axis)
For a perspective projection:
fovy (float): field of view in the y direction (height) in degrees
@@ -97,7 +105,8 @@ class CameraSensor(LinkSensor):
bottom (float): bottom screen (canvas) coordinate
top (float): top screen (canvas) coordinate
"""
super(CameraSensor, self).__init__(simulator, body_id, link_id, position, orientation, rate)
super(CameraSensor, self).__init__(simulator, body_id=body_id, link_id=link_id, noise=noise, ticks=ticks,
latency=latency, position=position, orientation=orientation)
self.width = int(width)
self.height = int(height)
@@ -168,20 +177,18 @@ class CameraSensor(LinkSensor):
"""
Return the captured RGBA image. 'A' stands for alpha channel (for opacity/transparency)
"""
img = self.sim.get_camera_image(self.width, self.height, self.getV(), self._P,
shadow=1, # lightDirection=[1,1,1],
# renderer=self.sim.ER_TINY_RENDERER)[2])
renderer=self.sim.ER_BULLET_HARDWARE_OPENGL)[2]
img = img.reshape(self.width, self.height, 4) # RGBA
img = self.sim.get_rgba_image(self.width, self.height, view_matrix=self.getV(), projection_matrix=self._P,
shadow=1, # lightDirection=[1,1,1],
# renderer=self.sim.ER_TINY_RENDERER)[2])
renderer=self.sim.ER_BULLET_HARDWARE_OPENGL)
return img
def get_depth_image(self):
"""
Return the depth image.
"""
img = self.sim.get_camera_image(self.width, self.height, self.getV(), self._P,
renderer=self.sim.ER_BULLET_HARDWARE_OPENGL)[3]
img = img.reshape(self.width, self.height)
img = self.sim.get_depth_image(self.width, self.height, view_matrix=self.getV(), projection_matrix=self._P,
renderer=self.sim.ER_BULLET_HARDWARE_OPENGL)
return img
def get_rgbad_image(self, concatenate=True):
@@ -189,23 +196,82 @@ class CameraSensor(LinkSensor):
Return the RGBA and depth images.
"""
rgba, depth = self.sim.get_camera_image(self.width, self.height, self.getV(), self._P)[2:4]
rgba = rgba.reshape(self.width, self.height, 4)
depth = depth.reshape(self.width, self.height)
if concatenate:
return np.dstack((rgba, depth))
return rgba, depth
_sense = get_rgbad_image
def _sense(self, apply_noise=True):
"""
Sense using the camera sensor.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
np.array[W,H,5]: RGBA and depth image concatenated together
"""
data = self.get_rgbad_image()
if apply_noise:
self._noise(data, inplace=True) # inplace otherwise can be expensive
return data
class DepthCameraSensor(CameraSensor):
r"""Depth Camera sensor.
"""
_sense = CameraSensor.get_depth_image
def _sense(self, apply_noise=True):
"""
Sense using the depth camera sensor.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
np.array[W,H]: depth image
"""
data = self.get_depth_image()
if apply_noise:
self._noise(data, inplace=True) # inplace otherwise can be expensive
return data
class Camera2DSensor(CameraSensor):
r"""2D camera sensor
class RGBCameraSensor(CameraSensor):
r"""RGB camera sensor
"""
_sense = CameraSensor.get_rgb_image
def _sense(self, apply_noise=True):
"""
Sense using the camera RGB sensor.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
np.array[W,H]: depth image
"""
data = self.get_rgb_image()
if apply_noise:
self._noise(data, inplace=True) # inplace otherwise can be expensive
return data
# class SegmentationCameraSensor(CameraSensor):
# r"""Segmentation Camera sensor.
# """
#
# def _sense(self, apply_noise=True):
# """
# Sense using the camera RGB sensor.
#
# Args:
# apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
#
# Returns:
# np.array[W,H]: depth image
# """
# data = self.get_rgb_image()
# if apply_noise:
# self._noise(data, inplace=True) # inplace otherwise can be expensive
# return data
#
+48 -25
View File
@@ -20,16 +20,29 @@ class ContactSensor(LinkSensor):
This sensor return 1 if in contact with an object, and 0 otherwise.
"""
def __init__(self, simulator, body_id, link_id, position, orientation, rate=1):
super(ContactSensor, self).__init__(simulator, body_id, link_id, position, orientation, rate)
def __init__(self, simulator, body_id, link_id=-1, noise=None, ticks=1, latency=None):
"""
Initialize the contact sensor.
Args:
simulator (Simulator): simulator instance.
body_id (int): unique body id.
link_id (int): unique id of the link.
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
"""
super(ContactSensor, self).__init__(simulator, body_id=body_id, link_id=link_id, noise=noise, ticks=ticks,
latency=latency)
def get_contact_points(self):
"""Get the contact points.
"""
Get the contact points.
Returns:
list: list of contacts
"""
contacts = self.sim.get_contact_points(bodyA=self.body_id, linkIndexA=self.link_id)
contacts = self.sim.get_contact_points(bodyA=self.body_id, link1_id=self.link_id)
return contacts
def is_in_contact(self):
@@ -38,27 +51,37 @@ class ContactSensor(LinkSensor):
"""
return len(self.get_contact_points()) > 0
# alias
_sense = is_in_contact
class PressureSensor(LinkSensor):
r"""Pressure sensor.
Compared to the binary contact sensor, this sensor returns a continuous pressure value.
"""
def __init__(self, simulator, body_id, link_id):
super(PressureSensor, self).__init__(simulator, body_id, link_id)
def _sense(self):
raise NotImplementedError
class TouchSensor(LinkSensor):
r"""Touch Sensor
"""
pass
def _sense(self, apply_noise=True):
"""
Sense using the contact sensor.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
bool: True if there is contact.
"""
# there is already some noise from the simulator
return self.is_in_contact()
# class PressureSensor(LinkSensor):
# r"""Pressure sensor.
#
# Compared to the binary contact sensor, this sensor returns a continuous pressure value.
# """
#
# def __init__(self, simulator, body_id, link_id):
# super(PressureSensor, self).__init__(simulator, body_id, link_id)
#
# def _sense(self, apply_noise=True):
# raise NotImplementedError
#
#
# class TouchSensor(LinkSensor):
# r"""Touch Sensor
# """
# pass
#
#
# class SkinPressureSensor
+88 -8
View File
@@ -16,15 +16,95 @@ __email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class ForceTorqueSensor(JointSensor):
r"""Force-Torque (FT or F/T) Sensor
class JointTorqueSensor(JointSensor):
r"""Joint Torque sensor.
The F/T sensor allows to measure the forces and torques applied to it.
"""
def __init__(self, simulator, body_id, joint_id, position, orientation, rate=1):
super(ForceTorqueSensor, self).__init__(simulator, body_id, joint_id, position, orientation, rate)
self.sim.enable_joint_force_torque_sensor(body_id, joint_id, enableSensor=True)
def __init__(self, simulator, body_id, joint_ids, noise=None, ticks=1, latency=None):
"""
Initialize the F/T sensor.
def _sense(self):
return np.array(self.sim.getJointState(self.body_id, self.joint_id)[2])
Args:
simulator (Simulator): simulator
body_id (int): unique body id.
joint_ids (int, int[N], None): joint id, or list of joint ids. If None, it will get all the actuated joints.
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
"""
super(JointTorqueSensor, self).__init__(simulator, body_id=body_id, joint_ids=joint_ids, noise=noise,
ticks=ticks, latency=latency)
# enable the F/T sensors
self.sim.enable_joint_force_torque_sensor(body_id, joint_ids=self.joint_ids, enable=True)
def _sense(self, apply_noise=True):
"""Sense the force/torques values.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
np.array[N]: torque values
"""
# check if the simulator supports that sensor
if self.sim.supports_sensors("torque"):
return self.sim.get_sensor("torque", self.body_id, self.joint_ids).sense().reshape(-1)
torques = self.sim.get_joint_torques(self.body_id, self.joint_ids).reshape(-1)
if apply_noise:
torques = self._noise(torques)
return torques
class JointForceTorqueSensor(JointSensor):
r"""Joint Force-Torque (FT or F/T) Sensor
The F/T sensor allows to measure the forces and torques applied to it; "it reports the joint reaction forces in
the fixed degrees of freedom: a fixed joint will measure all 6DOF joint forces/torques. A revolute joint
force/torque sensor will measure 5DOF reaction forces along all axis except the revolute axis." [1]
Note that this work with fixed joints as well.
References:
- [1] Pybullet
"""
def __init__(self, simulator, body_id, joint_ids, noise=None, ticks=1, latency=None):
"""
Initialize the joint F/T sensor.
Args:
simulator (Simulator): simulator
body_id (int): unique body id.
joint_ids (int, int[N], None): joint id, or list of joint ids. If None, it will get all the actuated joints.
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
"""
super(JointForceTorqueSensor, self).__init__(simulator, body_id=body_id, joint_ids=joint_ids, noise=noise,
ticks=ticks, latency=latency)
# enable the F/T sensors
self.sim.enable_joint_force_torque_sensor(body_id, joint_ids=self.joint_ids, enable=True)
def _sense(self, apply_noise=True):
"""Sense the force/torques values.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
np.array[6*N]: F/T values
"""
# check if the simulator supports that sensor
if self.sim.supports_sensors("force-torque"):
return self.sim.get_sensor("force-torque", self.body_id, self.joint_ids).sense().reshape(-1)
forces = self.sim.get_joint_reaction_forces(self.body_id, self.joint_ids).reshape(-1)
if apply_noise:
forces = self._noise(forces)
return forces
+64 -7
View File
@@ -3,13 +3,19 @@
'An inertial measurement unit (IMU) is an electronic device that measures and reports a body's specific force,
angular rate, and sometimes the magnetic field surroundings the body, using a combination of accelerometers
and gyroscopes, sometimes also magnetometers' (Wikipedia)
and gyroscopes, sometimes also magnetometers' [1]
References:
- [1] Inertial measurement unit: https://en.wikipedia.org/wiki/Inertial_measurement_unit
"""
import numpy as np
import pyrobolearn as prl
from pyrobolearn.robots.sensors.links import LinkSensor
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__copyright__ = "Copyright 2019, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "GNU GPLv3"
__version__ = "1.0.0"
@@ -24,13 +30,64 @@ class IMUSensor(LinkSensor):
The IMU sensor measures the linear and angular motion using accelerometers and gyroscopes. Specifically,
3-axis accelerometers allow to measure the accelerations along its axes, and 3-axis gyroscopes measure the
angular velocities around its axes. Sometimes, theses sensors also have a 3-axis magnetometer which measures
the magnetic field.
the magnetic field. [1]
Note that real IMUs typically accumulate errors over time, and thus the integrated values drift over time.
A very wide variety of IMUs exists, depending on application types, with performance ranging [:
- from 0.1 deg/s to 0.001 deg/h for gyroscope
- from 100 mg to 10^{-6}g for accelerometers
References:
- [1] Inertial measurement unit: https://en.wikipedia.org/wiki/Inertial_measurement_unit
- [2] "IMU, what for: performance per application infographic":
https://www.thalesgroup.com/en/worldwide/aerospace/topaxyz-inertial-measurement-unit-imu/infographic
"""
def __init__(self, simulator, body_id, link_id, position, orientation):
super(IMUSensor, self).__init__(simulator, body_id, link_id, position, orientation)
def __init__(self, simulator, body_id, link_id=-1, noise=None, ticks=1, latency=None, position=None,
orientation=None):
"""
Initialize the IMU sensor.
def _sense(self):
pass
Args:
simulator (Simulator): simulator instance.
body_id (int): unique body id.
link_id (int): unique id of the link.
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
position (np.array[3], None): local position of the sensor with respect to the given link. If None, it will
be the zero vector.
orientation (np.array[4], None): local orientation of the sensor with respect to the given link (expressed
as a quaternion [x,y,z,w]). If None, it will be the unit quaternion [0,0,0,1].
"""
super(IMUSensor, self).__init__(simulator, body_id=body_id, link_id=link_id, noise=noise, ticks=ticks,
latency=latency, position=position, orientation=orientation)
def _sense(self, apply_noise=True):
"""Sense using the IMU sensor.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
np.array[6]: concatenation of linear accelerations and angular velocities
"""
# if the simulator supports IMU sensors, return the sensed data
if self.simulator.supports_sensors("imu"):
return self.simulator.get_sensor("imu", self.body_id, self.link_id).sense()
# linear acceleration
acceleration = self.get_link_world_acceleration()[:3]
# angular velocity
angular_velocity = self.get_link_world_velocity()[3:]
# concatenate the data and apply the noise
data = np.concatenate((acceleration, angular_velocity))
if apply_noise:
data = self._noise(data)
# return the noisy data
return data
+240 -26
View File
@@ -5,7 +5,9 @@ This mainly include encoders.
"""
import copy
from abc import ABCMeta, abstractmethod
import time
from abc import ABCMeta
import numpy as np
from pyrobolearn.robots.sensors.sensor import Sensor
@@ -26,35 +28,167 @@ class JointSensor(Sensor):
"""
__metaclass__ = ABCMeta
def __init__(self, simulator, body_id, joint_id, position=None, orientation=None, rate=1):
def __init__(self, simulator, body_id, joint_ids=None, noise=None, ticks=1, latency=None):
"""Initialize the sensor.
Args:
simulator (Simulator): simulator
body_id (int): unique id of the body
joint_id (int): unique id of the joint
position (vec3): local position of the sensor with respect to the given joint
orientation (vec4): local orientation of the sensor with respect to the given joint
rate (int): number of steps to wait before acquisition of the next sensor value.
body_id (int): unique body id.
joint_ids (int, int[N], None): joint id, or list of joint ids. If None, it will get all the actuated joints.
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
"""
super(JointSensor, self).__init__(simulator, body_id, position, orientation, rate)
self.joint_id = joint_id
super(JointSensor, self).__init__(simulator, body_id=body_id, noise=noise, ticks=ticks, latency=latency)
@property
def position(self):
"""
Return the joint position
"""
return self.sim.get_joint_state(self.body_id, self.joint_id)[0]
# set the joint ids
if joint_ids is None:
# get actuated joints
joint_ids = []
for joint_id in range(self.sim.num_joints(self.body_id)):
joint_info = self.sim.get_joint_info(self.body_id, joint_id)
if joint_info[2] != self.sim.JOINT_FIXED: # if not a fixed joint
joint_ids.append(joint_info[0])
elif isinstance(joint_ids, int):
joint_ids = [joint_ids]
elif isinstance(joint_ids, (tuple, list)):
for i, joint in enumerate(joint_ids):
if not isinstance(joint, int):
raise TypeError("Expecting the given {}th joint id to be an int, instead got: {}".format(i, joint))
else:
raise TypeError("Expecting the given 'joint_ids' to be an int or list of int, instead got: "
"{}".format(joint_ids))
self.joint_ids = joint_ids
self.q_indices = self.sim.get_q_indices(self.body_id, self.joint_ids)
@abstractmethod
def _sense(self):
raise NotImplementedError
# joint state
self._state = {}
self._prev_state = {}
#############
# Operators #
#############
def clean(self):
"""Clean sensor values."""
# update previous and current states
self._prev_state = self._state
self._state = {}
def get_joint_positions(self):
r"""
Get the position of the given joint(s).
Returns:
if 1 joint:
float: joint position [rad]
if multiple joints:
np.array[N]: joint positions [rad]
"""
# check if cached
if 'q' in self._state:
# get cached joint positions
q = self._state['q'][0]
else:
# get joint positions and cache it
q = self.sim.get_joint_positions(self.body_id, self.joint_ids)
self._state['q'] = [q, time.time()]
# return joint positions
return q[self.q_indices]
def get_joint_velocities(self):
r"""
Get the velocity of the given joint(s).
Returns:
if 1 joint:
float: joint velocity [rad/s]
if multiple joints:
np.array[N]: joint velocities [rad/s]
"""
# check if cached
if 'dq' in self._state:
# get cached joint velocities
dq = self._state['dq'][0]
else:
# get joint velocities and cache it
dq = self.sim.get_joint_velocities(self.body_id, self.joint_ids)
self._state['dq'] = [dq, time.time()]
# return joint velocities
return dq[self.q_indices]
def get_joint_accelerations(self):
r"""
Get the acceleration of the specified joint(s). If the simulator doesn't provide the joint accelerations, this
is computed using finite difference :math:`\ddot{q}(t) = \frac{\dot{q}(t) - \dot{q}(t-dt)}{dt}`.
Warnings: if we use finite difference, note that the first time this method is called, it will return a zero
vector because we do not have previous joint velocities (i.e. :math:`\dot{q}(t-dt)`) yet.
Returns:
if 1 joint:
float: joint acceleration [rad/s^2]
if multiple joints:
np.array[N]: joint accelerations [rad/s^2]
"""
# check if cached
if 'ddq' in self._state:
ddq = self._state[0]
return ddq[self.q_indices]
# if simulator supports accelerations
if self.sim.supports_acceleration():
return self.sim.get_joint_accelerations(self.body_id, self.joint_ids)
# else, use finite difference
# get current joint velocities and time
if 'dq' in self._state:
dq, t = self._state['dq']
else:
dq, t = self.get_joint_velocities(), time.time()
self._state['dq'] = [dq, t]
# if we did not cache the previous joint velocities, return zero vector for accelerations
if 'dq' not in self._prev_state:
ddq = np.zeros(len(self.joint_ids))
self._state['ddq'] = [ddq, t]
return ddq[self.q_indices]
# retrieve previous joint velocities and time
dq_prev, t_prev = self._prev_state['dq']
# compute time difference
if self.sim.use_real_time(): # if the simulator is in real-time mode
dt = (t - t_prev)
else: # if we are stepping in the simulator
dt = self.sim.timestep
# compute joint accelerations using finite difference, and cache it
ddq = (dq - dq_prev) / dt
self._state['ddq'] = [ddq, t]
# return joint accelerations
return ddq[self.q_indices]
def get_joint_torques(self):
r"""
Get the applied torque on the given joint(s).
Returns:
if 1 joint:
float: torque [Nm]
if multiple joints:
np.array[N]: torques associated to the given joints [Nm]
"""
return self.sim.get_joint_torques(self.body_id, self.joint_ids)
def __copy__(self):
"""Return a shallow copy of the sensor. This can be overridden in the child class."""
return self.__class__(simulator=self.simulator, body_id=self.body_id, joint_id=self.joint_id,
position=self.local_position, orientation=self.local_orientation, rate=self.rate)
return self.__class__(simulator=self.simulator, body_id=self.body_id, joint_id=self.joint_ids,
position=self.local_position, orientation=self.local_orientation, rate=self._ticks)
def __deepcopy__(self, memo={}):
"""Return a deep copy of the sensor. This can be overridden in the child class.
@@ -64,21 +198,101 @@ class JointSensor(Sensor):
"""
simulator = memo.get(self.simulator, self.simulator) # copy.deepcopy(self.simulator, memo)
body_id = copy.deepcopy(self.body_id)
joint_id = copy.deepcopy(self.joint_id)
joint_id = copy.deepcopy(self.joint_ids)
position = copy.deepcopy(self.local_position)
orientation = copy.deepcopy(self.local_orientation)
sensor = self.__class__(simulator=simulator, body_id=body_id, joint_id=joint_id, position=position,
orientation=orientation, rate=self.rate)
orientation=orientation, rate=self._ticks)
memo[self] = sensor
return sensor
class Encoder(JointSensor):
r"""Encoder joint sensor
class JointEncoderSensor(JointSensor):
r"""Joint encoder sensor
The encoder is a sensor that measures rotation allowing to determine the angle, displacement, velocity, or
acceleration.
References:
- [1] Robot encoder: https://www.societyofrobots.com/sensors_encoder.shtml
"""
def __init__(self, simulator, body_id, joint_id, noise=None):
super(Encoder, self).__init__(simulator, body_id, joint_id)
def __init__(self, simulator, body_id, joint_ids=None, noise=None, ticks=1, latency=None):
"""
Initialize the joint encoder sensor.
Args:
simulator (Simulator): simulator
body_id (int): unique body id.
joint_ids (int, int[N], None): joint id, or list of joint ids. If None, it will get all the actuated joints.
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
"""
super(JointEncoderSensor, self).__init__(simulator, body_id=body_id, joint_ids=joint_ids, noise=noise,
ticks=ticks, latency=latency)
# sense once
self._sense()
##############
# Properties #
##############
@property
def q(self):
"""Return the sensed joint position values."""
return self._data[:len(self.joint_ids)]
@property
def dq(self):
"""Return the sensed joint velocity values."""
return self._data[len(self.joint_ids):2*len(self.joint_ids)]
@property
def ddq(self):
"""Return the sensed joint acceleration values."""
return self._data[2*len(self.joint_ids):]
###########
# Methods #
###########
def get_sensed_joint_positions(self):
"""Return the sensed joint positions."""
return self.q
def get_sensed_joint_velocities(self):
"""Return the sensed joint velocities."""
return self.dq
def get_sensed_joint_accelerations(self):
"""Return the sensed joint accelerations."""
return self.ddq
def _sense(self, apply_noise=True):
"""
Sense the joint (position, velocities, accelerations) values.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
np.array[3*N]: concatenation of joint positions, velocities and accelerations
"""
# if the simulator supports encoder sensors, return the sensed data
# if self.simulator.supports_sensors("encoder"):
# return self.simulator.get_sensor("encoder", self.body_id, self.joint_ids).sense()
# joint state #
positions = self.get_joint_positions()
velocities = self.get_joint_velocities()
accelerations = self.get_joint_accelerations()
# concatenate the data and apply the noise
data = np.concatenate((positions, velocities, accelerations))
if apply_noise:
data = self._noise(data)
# return the noisy data
return data
+9 -1
View File
@@ -7,7 +7,7 @@ import numpy as np
from pyrobolearn.robots.sensors.links import LinkSensor
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__copyright__ = "Copyright 2019, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "GNU GPLv3"
__version__ = "1.0.0"
@@ -26,6 +26,7 @@ class LaserSensor(LinkSensor):
r"""Laser Sensor
"""
pass
class ProximityLaserSensor(LaserSensor):
@@ -35,6 +36,13 @@ class ProximityLaserSensor(LaserSensor):
pass
class RaySensor(LaserSensor):
r"""
"""
pass
class IRProximitySensor(ProximityLaserSensor):
r"""Infrared proximity sensor
+149 -20
View File
@@ -5,9 +5,11 @@ These include IMU, contact, Camera, and other sensors.
"""
import copy
from abc import ABCMeta, abstractmethod
import time
import numpy as np
from abc import ABCMeta
from pyrobolearn.utils.transformation import get_quaternion_product
from pyrobolearn.utils.transformation import get_quaternion_product, get_rotated_point_from_quaternion
from pyrobolearn.robots.sensors.sensor import Sensor
@@ -29,27 +31,48 @@ class LinkSensor(Sensor):
__metaclass__ = ABCMeta
def __init__(self, simulator, body_id, link_id=None, position=None, orientation=None, rate=1):
"""Initialize the sensor.
def __init__(self, simulator, body_id, link_id=-1, position=None, orientation=None, noise=None, ticks=1,
latency=None):
"""Initialize the link sensor.
Args:
simulator (Simulator): simulator
body_id (int): unique id of the body
link_id (int): unique id of the link
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.
simulator (Simulator): simulator instance.
body_id (int): unique id of the body.
link_id (int): unique id of the link.
position (np.array[3], None): local position of the sensor with respect to the given link. If None, it will
be the zero vector.
orientation (np.array[4], None): local orientation of the sensor with respect to the given link (expressed
as a quaternion [x,y,z,w]). If None, it will be the unit quaternion [0,0,0,1].
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
"""
super(LinkSensor, self).__init__(simulator, body_id, position, orientation, rate)
super(LinkSensor, self).__init__(simulator, body_id=body_id, position=position, orientation=orientation,
noise=noise, ticks=ticks, latency=latency)
# set link id
if not isinstance(link_id, int):
raise TypeError("Expecting the given 'link_id' to be an int, instead got: {}".format(type(link_id)))
self.link_id = link_id
# link state
self._state = {}
self._prev_state = {}
##############
# Properties #
##############
@property
def position(self):
"""
Return the link position in the Cartesian world frame.
"""
position = self.sim.get_link_state(self.body_id, self.link_id)[0]
position += self.local_position
position = self.sim.get_link_state(self.body_id, self.link_id)[0] # world position of CoM (o^w_1)
orientation = self.sim.get_link_state(self.body_id, self.link_id)[1] # world orientation of CoM (R^w_1)
# p^w = o^w_1 + R^w_1 p^1
# TODO: check if we are giving p^1 or p^2 --> p^w = o^w_1 + R^w_1 R^1_2 p^2 ??
# The local position that we are giving, is it in the new orientated frame or the local frame?
position += get_rotated_point_from_quaternion(orientation, self.local_position)
return position
@property
@@ -57,18 +80,124 @@ class LinkSensor(Sensor):
"""
Return the link orientation in the Cartesian world frame.
"""
orientation = self.sim.get_link_state(self.body_id, self.link_id)[1]
orientation = get_quaternion_product(self.local_orientation, orientation)
orientation = self.sim.get_link_state(self.body_id, self.link_id)[1] # world orientation of CoM (R^w_1)
# R^w_2 = R^w_1 R^1_2
orientation = get_quaternion_product(orientation, self.local_orientation)
return orientation
@abstractmethod
def _sense(self):
raise NotImplementedError
###########
# Methods #
###########
def clean(self):
"""Clean sensor values."""
# update previous and current states
self._prev_state = self._state
self._state = {}
def get_link_world_position(self):
r"""
Return the CoM position (in the Cartesian world space coordinates) of the link associated with the sensor.
Returns:
np.array[3]: the link CoM position in the world space
"""
# check if cached
if 'pos' in self._state:
pos = self._state['pos'][0] # (3,)
else:
if self.link_id == -1: # base link
pos = self.sim.get_base_position(self.body_id)
else: # other link
pos = self.sim.get_link_world_positions(body_id=self.body_id, link_ids=self.link_id) # (3,)
self._state['pos'] = [pos, time.time()]
# return position
return pos
def get_link_world_velocity(self):
r"""
Return the linear and angular velocities (expressed in the Cartesian world space coordinates) of the link
associated with the sensor.
Returns:
np.array[6]: linear and angular velocity of the link in the Cartesian world space
"""
# check if cached
if 'vel' in self._state:
vel = self._state['vel'][0] # (6,)
else:
if self.link_id == -1: # base link
vel = np.concatenate(self.sim.get_base_velocity(self.body_id))
else: # other link
vel = self.sim.get_link_world_velocities(body_id=self.body_id, link_ids=self.link_id)
self._state['vel'] = [vel, time.time()]
# return velocity
return vel
def get_link_world_acceleration(self):
r"""
Return the linear and angular accelerations (expressed in the Cartesian world space coordinates) of the link
associated with the sensor.
Returns:
np.array[6]: linear and angular acceleration of the link in the Cartesian world space
"""
# check if cached
if 'acc' in self._state:
acc = self._state['acc'][0]
else:
# if the simulator keep in memory the accelerations, return it
if self.sim.supports_acceleration():
acc = self.sim.get_link_world_accelerations(self.body_id, link_ids=self.link_id) # (6,)
else: # else, use finite difference
# get current link world velocities and time
if 'vel' not in self._state:
self.get_link_world_velocity()
vel, t = self._state['vel'] # (6,)
# if we did not cache the previous base velocity
if 'vel' not in self._prev_state:
acc = np.zeros(6) # (6,)
else:
# retrieve previous link world velocities and time
vel_prev, t_prev = self._prev_state['vel'] # (6,)
# compute time difference
if self.sim.use_real_time(): # if the simulator is in real-time mode
dt = (t - t_prev)
else: # if we are stepping in the simulator
dt = self.sim.timestep
# get current link positions
pos = self.get_link_world_position() # (3,)
# separate linear and angular velocities
lin_vel, ang_vel = vel[:3], vel[3:] # (3,)
lin_vel_prev, ang_vel_prev = vel_prev[:3], vel_prev[3:] # (3,)
# compute base acceleration
ang_acc = (ang_vel - ang_vel_prev) / dt
lin_acc = (lin_vel - lin_vel_prev) / dt
lin_acc += np.cross(ang_acc, pos) + np.cross(ang_vel, np.cross(ang_vel, pos))
acc = np.concatenate((lin_acc, ang_acc)) # (6,)
# cache the acceleration
self._state['acc'] = [acc, time.time()]
return acc
#############
# Operators #
#############
def __copy__(self):
"""Return a shallow copy of the sensor. This can be overridden in the child class."""
return self.__class__(simulator=self.simulator, body_id=self.body_id, link_id=self.link_id,
position=self.local_position, orientation=self.local_orientation, rate=self.rate)
position=self.local_position, orientation=self.local_orientation, rate=self._ticks)
def __deepcopy__(self, memo={}):
"""Return a deep copy of the sensor. This can be overridden in the child class.
@@ -82,6 +211,6 @@ class LinkSensor(Sensor):
position = copy.deepcopy(self.local_position)
orientation = copy.deepcopy(self.local_orientation)
sensor = self.__class__(simulator=simulator, body_id=body_id, link_id=link_id, position=position,
orientation=orientation, rate=self.rate)
orientation=orientation, rate=self._ticks)
memo[self] = sensor
return sensor
+78 -67
View File
@@ -2,7 +2,7 @@
"""Define miscellaneous sensors.
"""
from pyrobolearn.robots.sensors.links import Sensor, LinkSensor
from pyrobolearn.robots.sensors.links import LinkSensor
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
@@ -14,76 +14,87 @@ __email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class HeightmapSensor(Sensor):
r"""Heightmap Sensor
Sensor that detects the heights of its surrounding using a grid map.
"""
def __init__(self, simulator, body_id, link_id, width, height, num_rays_width, num_rays_height,
position=None, orientation=None):
"""Initialize the heightmap sensor.
Note that `num_rays_width * num_rays_height` has to be smaller than `simulator.MAX_RAY_INTERSECTION_BATCH_SIZE`.
In pybullet, this is currently set to be 256.
Args:
simulator (Simulator): simulator
body_id (int): unique id of the body
link_id (int): unique id of the link
position: local position of the sensor with respect to the given link
orientation: local orientation of the sensor with respect to the given link
width (float): width of the map (along the left-right axis of the body, measured in meters)
height (float): height of the map (along the front-back axis of the body, measured in meters)
num_rays_width (int): number of rays along the width dimension (left-right axis). This will be the 'width'
of the returned heightmap.
num_rays_height (int): number of rays along the height dimension (front-back axis). This will be
the 'height' of the returned heightmap.
"""
super(HeightmapSensor, self).__init__(simulator, body_id, link_id, position, orientation)
# Check arguments
if not isinstance(num_rays_width, int):
raise TypeError("num_rays_width needs to be an integer")
if not isinstance(num_rays_height, int):
raise TypeError("num_rays_height needs to be an integer")
if num_rays_width * num_rays_height > self.sim.MAX_RAY_INTERSECTION_BATCH_SIZE: # pybullet = 256
raise ValueError("num_rays_width * num_rays_height can not be bigger"
" than {}".format(self.sim.MAX_RAY_INTERSECTION_BATCH_SIZE))
if num_rays_width < 2:
raise ValueError("num_rays_width must be equal or bigger than 2")
if num_rays_height < 2:
raise ValueError("num_rays_height must be equal or bigger than 2")
# construct the grid
self.width_step = float(width) / num_rays_width
self.height_step = float(height) / num_rays_height
def get_ray_from_positions(self):
# using width and height
pass
def get_ray_to_positions(self):
pass
def _sense(self, normalize=False, display=False):
"""Return the heightmap.
Returns:
np.array: Height map with shape [width, height] where the values are the heights (in meters).
"""
# calculate width and height
collisions = self.sim.ray_test_batch(self.get_ray_from_positions(), self.get_ray_to_positions())
return collisions
class TemperatureSensor(LinkSensor):
pass
r"""Temperature sensor
A temperature sensor measures the temperature and converts it into an electrical signal.
"""
raise NotImplementedError
class UltrasonicSensor(LinkSensor):
pass
r"""Ultrasonic Sensor
"Ultrasonic transducers or ultrasonic sensors are a type of acoustic sensor divided into three broad categories:
transmitters, receivers and transceivers. Transmitters convert electrical signals into ultrasound, receivers
convert ultrasound into electrical signals, and transceivers can both transmit and receive ultrasound." [1]
References:
- [1] Ultrasonic transducer (Wikipedia): https://en.wikipedia.org/wiki/Ultrasonic_transducer
"""
raise NotImplementedError
class TransmitterSensor(UltrasonicSensor):
r"""Transmitter sensor
"The transmitter sensor is an acoustic sensor which converts the electrical signals into ultrasounds." [1]
References:
- [1] Ultrasonic transducer (Wikipedia): https://en.wikipedia.org/wiki/Ultrasonic_transducer
"""
raise NotImplementedError
class ReceiverSensor(UltrasonicSensor):
r"""Receiver sensor
"The receiver sensor is an acoustic sensor which converts ultrasound into electrical signals." [1]
References:
- [1] Ultrasonic transducer (Wikipedia): https://en.wikipedia.org/wiki/Ultrasonic_transducer
"""
raise NotImplementedError
class TransceiverSensor(UltrasonicSensor):
r"""Receiver sensor
"The transceiver sensor can both transmit and receive ultrasound by converting into/from electrical signals." [1]
References:
- [1] Ultrasonic transducer (Wikipedia): https://en.wikipedia.org/wiki/Ultrasonic_transducer
"""
raise NotImplementedError
class HumiditySensor(LinkSensor):
pass
r"""Humidity sensor
A humidity sensor measures and reports the moisture and air temperature.
"""
raise NotImplementedError
class GPSSensor(LinkSensor):
r"""GPS sensor
"The GPS is a satellite-based radionavigation system. Autonomous robots use a GPS sensors to get the latitude,
longitude, time, speed, and heading." [1]
References:
- [1] Global Positioning System (Wikipedia): https://en.wikipedia.org/wiki/Global_Positioning_System
"""
raise NotImplementedError
class MagnetometerSensor(LinkSensor):
r"""Magnetometer Sensor
"A magnetometer is a device that measures magnetism - the direction, strength, or relative change of a magnetic
field at a particular location." [1]
References:
- [1] Magnetometer (Wikipedia): https://en.wikipedia.org/wiki/Magnetometer
"""
raise NotImplementedError
+248
View File
@@ -0,0 +1,248 @@
#!/usr/bin/env python
"""Define ray sensors; sensors that cast rays into the world and return the range of the nearest object that were
intersected with these ones.
"""
import numpy as np
from pyrobolearn.robots.sensors.links import LinkSensor
from pyrobolearn.utils.transformation import get_rotated_point_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 RaySensor(LinkSensor):
r"""Ray sensor
This sensor casts a single ray into the world, check for intersection, and return the range of the nearest object.
"""
def __init__(self, simulator, body_id, to_position, link_id=-1, noise=None, ticks=1, latency=None, position=None,
orientation=None):
"""
Initialize the Ray sensor.
Args:
simulator (Simulator): simulator instance.
body_id (int): unique body id.
to_position (np.array[3]): position where the ray should stop with respect to the new local link frame
(specified by :attr:`position` and :attr:`orientation`).
link_id (int): unique id of the link.
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
position (np.array[3], None): local position of the sensor with respect to the given link. If None, it will
be the zero vector.
orientation (np.array[4], None): local orientation of the sensor with respect to the given link (expressed
as a quaternion [x,y,z,w]). If None, it will be the unit quaternion [0,0,0,1].
"""
super(RaySensor, self).__init__(simulator, body_id=body_id, link_id=link_id, noise=noise, ticks=ticks,
latency=latency, position=position, orientation=orientation)
if isinstance(to_position, (tuple, list)):
to_position = np.asarray(to_position)
if not isinstance(to_position, np.ndarray):
raise TypeError("Expecting the given 'to_position' to be a np.array, but got instead: "
"{}".format(type(to_position)))
if to_position.shape != (3,):
raise ValueError("Expecting the shape of the given 'to_position' to be (3,), but got instead a shape of: "
"{}".format(to_position.shape))
self.to_position = to_position
def _sense(self, apply_noise=True):
"""
Sense using the ray sensor.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
float: hit fraction along the ray in range [0,1] along the ray.
"""
if self.simulator.supports_sensors("ray"):
return self.simulator.get_sensor("ray", self.body_id, self.link_id).sense()
position = self.position + get_rotated_point_from_quaternion(self.orientation, self.to_position)
hit = self.sim.ray_test(from_position=self.position, to_position=position)[2]
if apply_noise:
hit = self._noise(hit)
return hit
class RayBatchSensor(LinkSensor):
r"""Ray batch sensor.
This sensor casts a batch of rays into the world, check for intersections, and return the range of the nearest
objects. This can be used for sonars, laser scanning range sensors (such as LIDAR), and others.
Note that the number of rays must be smaller than `simulator.MAX_RAY_INTERSECTION_BATCH_SIZE`. In pybullet, this
is currently set to 16,384.
"""
def __init__(self, simulator, body_id, to_positions, link_id=-1, noise=None, ticks=1, latency=None, position=None,
orientation=None):
"""
Initialize the Ray batch sensor.
Args:
simulator (Simulator): simulator instance.
body_id (int): unique body id.
to_positions (np.array[N,3]): position where each ray should stop with respect to the new local link frame
(specified by :attr:`position` and :attr:`orientation`).
link_id (int): unique id of the link.
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
position (np.array[3], None): local position of the sensor with respect to the given link. If None, it will
be the zero vector.
orientation (np.array[4], None): local orientation of the sensor with respect to the given link (expressed
as a quaternion [x,y,z,w]). If None, it will be the unit quaternion [0,0,0,1].
"""
super(RayBatchSensor, self).__init__(simulator, body_id=body_id, link_id=link_id, noise=noise, ticks=ticks,
latency=latency, position=position, orientation=orientation)
if isinstance(to_positions, (tuple, list)):
to_positions = np.asarray(to_positions)
if not isinstance(to_positions, np.ndarray):
raise TypeError("Expecting the given 'to_positions' to be a np.array, but got instead: "
"{}".format(type(to_positions)))
if to_positions.ndim != 2:
raise ValueError("Expecting the given 'to_positions' to be 2D array, but got instead a {}D "
"array".format(to_positions.ndim))
if to_positions.shape[1] != 3:
raise ValueError("Expecting the shape of the given 'to_positions' to be (N,3), but got instead a shape "
"of: {}".format(to_positions.shape))
if len(to_positions) > self.sim.MAX_RAY_INTERSECTION_BATCH_SIZE:
raise ValueError("The number of 'to_positions' (={}) is bigger than the maximum amount authorized "
"(={})".format(len(to_positions), self.sim.MAX_RAY_INTERSECTION_BATCH_SIZE))
self.to_positions = to_positions
def _sense(self, apply_noise=True):
"""
Sense using the ray batch sensor.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
np.array[N]: hit fractions along each ray in range [0,1] along the ray.
"""
if self.simulator.supports_sensors("ray_batch"):
return self.simulator.get_sensor("ray_batch", self.body_id, self.link_id).sense()
position = self.position + get_rotated_point_from_quaternion(self.orientation, self.to_positions)
rays = self.sim.ray_test_batch(from_positions=self.position, to_positions=position)
hit = np.array([ray[2] for ray in rays])
if apply_noise:
hit = self._noise(hit)
return hit
class HeightmapSensor(LinkSensor):
r"""Heightmap Sensor
Sensor that detects the heights of its surrounding using a grid map.
Warnings: this is only valid in the simulator.
"""
def __init__(self, simulator, body_id, link_id, width, height, num_rays_width=2, num_rays_height=2,
max_ray_length=100, position=None, orientation=None): # TODO: use orientation initially
"""
Initialize the heightmap sensor. This is only valid in the simulator.
Note that `num_rays_width * num_rays_height` has to be smaller than `simulator.MAX_RAY_INTERSECTION_BATCH_SIZE`.
In pybullet, this is currently set to 16,384.
Args:
simulator (Simulator): simulator instance.
body_id (int): unique id of the body
link_id (int): unique id of the link
width (float): width of the map (along the left-right axis (i.e. y axis) of the body, measured in meters)
height (float): height of the map (along the front-back axis (i.e. x axis) of the body, measured in meters)
num_rays_width (int): number of rays along the width dimension (left-right axis). This will be the 'width'
of the returned heightmap. This must be bigger or equal to 2.
num_rays_height (int): number of rays along the height dimension (front-back axis). This will be
the 'height' of the returned heightmap. This must be bigger or equal to 2.
max_ray_length (float): maximum length of each ray.
position (np.array[3], None): local position of the sensor with respect to the given link. If None, it will
be the zero vector. This position represents the center of the map.
orientation (np.array[4], None): local orientation of the sensor with respect to the given link (expressed
as a quaternion [x,y,z,w]). If None, it will be the unit quaternion [0,0,0,1].
"""
super(HeightmapSensor, self).__init__(simulator, body_id=body_id, link_id=link_id, position=position,
orientation=orientation)
# Check arguments
# set num_rays_width and num_rays_height
num_rays_width, num_rays_height = int(num_rays_width), int(num_rays_height)
if num_rays_width * num_rays_height > self.sim.MAX_RAY_INTERSECTION_BATCH_SIZE: # pybullet = 16,384
raise ValueError("num_rays_width * num_rays_height can not be bigger"
" than {}".format(self.sim.MAX_RAY_INTERSECTION_BATCH_SIZE))
if num_rays_width < 2:
raise ValueError("num_rays_width must be equal or bigger than 2, but got: {}".format(num_rays_width))
if num_rays_height < 2:
raise ValueError("num_rays_height must be equal or bigger than 2, but got: {}".format(num_rays_height))
self._num_rays_width = num_rays_width
self._num_rays_height = num_rays_height
# set max_ray_length
if not isinstance(max_ray_length, (float, int)):
raise TypeError("Expecting 'max_ray_length' to be an int or float, but got instead: "
"{}".format(type(max_ray_length)))
max_ray_length = float(max_ray_length)
if max_ray_length <= 0.:
raise ValueError("Expecting 'max_ray_length' to be positive, but got instead: {}".format(max_ray_length))
self._max_ray_length = max_ray_length
# set width and height
width, height = float(width), float(height)
if width <= 0.:
raise ValueError("Expecting the 'width' to be bigger than 0, but got: {}".format(width))
if height <= 0.:
raise ValueError("Expecting the 'height' to be bigger than 0, but got: {}".format(height))
self._width = width
self._height = height
self._z_array = np.ones(self._num_rays_width * self._num_rays_height)
def get_ray_from_to_positions(self):
"""
Return the world positions for the rays to start and end.
Returns:
np.array[N,3]: list of starting positions for the rays
np.array[N,3]: list of ending positions for the rays
"""
pos = self.position
w2, h2 = self._width / 2., self._height / 2.
x, y = np.meshgrid(np.linspace(pos[1] - w2, pos[1] + w2, self._num_rays_width),
np.linspace(pos[0] - h2, pos[0] + h2, self._num_rays_height))
x, y = x.ravel(), y.ravel()
from_z = pos[2] * self._z_array
to_z = from_z - self._max_ray_length
from_positions = np.vstack((x, y, from_z)).T # (N, 3)
to_positions = np.vstack((x, y, to_z)).T # (N, 3)
return from_positions, to_positions
def _sense(self, apply_noise=True):
"""
Return the heightmap.
Returns:
np.array[width, height]: Height map with shape [width, height] where the values are the hit fractions [0,1],
you can multiply it by :attr:`max_ray_length` to get the depth in meters.
"""
from_positions, to_positions = self.get_ray_from_to_positions()
rays = self.sim.ray_test_batch(from_positions=from_positions, to_positions=to_positions)
hit = np.array([ray[2] for ray in rays]).reshape(self._num_rays_width, self._num_rays_height)
if apply_noise:
hit = self._noise(hit)
return hit
+131 -42
View File
@@ -8,14 +8,24 @@ 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
# TODO: convert ticks to rate when setting real-time
import sys
import copy
from abc import ABCMeta, abstractmethod
import numpy as np
from pyrobolearn.simulators.simulator import Simulator
from pyrobolearn.robots.base import Body
from pyrobolearn.robots.noise.noise import Noise, NoNoise
from pyrobolearn.utils.transformation import get_quaternion_product
# define long for Python 3.x
if int(sys.version[0]) == 3:
long = int
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
@@ -36,37 +46,80 @@ class Sensor(object): # sensor attached to a link or joint
"""
__metaclass__ = ABCMeta
def __init__(self, simulator, body_id, position=None, orientation=None, rate=1, latency=0):
"""Initialize the sensor.
def __init__(self, simulator, body_id, noise=None, ticks=1, latency=None, position=None, orientation=None):
"""
Initialize the sensor.
Args:
simulator (Simulator): simulator
body_id (int): unique id of the body
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.
simulator (Simulator): simulator instance.
body_id (int, Body): unique id of the body, or body instance.
noise (None, Noise): noise to be added.
ticks (int): number of steps to wait/sleep before acquisition of the next sensor value.
latency (int, float, None): latency time / step.
position (np.array[3], None): local position of the sensor with respect to the given link. If None, it will
be the zero vector.
orientation (np.array[4], None): local orientation of the sensor with respect to the given link (expressed
as a quaternion [x,y,z,w]). If None, it will be the unit quaternion [0,0,0,1].
"""
# setting simulator
if not isinstance(simulator, Simulator):
raise TypeError("Expecting the given 'simulator' to be an instance of `Simulator`, but got instead: "
"{}".format(type(simulator)))
self.sim = simulator
# set the body id
if isinstance(body_id, Body):
body_id = body_id.id
elif not isinstance(body_id, (int, long)):
raise TypeError("Expecting the given 'body_id' to be an int or an instance of `Body`, but got instead: "
"{}".format(type(body_id)))
if body_id < 0:
raise ValueError("Expecting the given 'body_id' to be a positive integer, but got instead: "
"{}".format(body_id))
self.body_id = body_id
# set the local position of the sensor
if position is None:
position = [0., 0., 0.]
self.local_position = np.array(position)
self.local_position = np.asarray(position)
# set the local orientation of the sensor
if orientation is None:
orientation = [0., 0., 0., 1.]
self.local_orientation = np.array(orientation)
self.local_orientation = np.asarray(orientation)
self.rate = rate
self.cnt = -1
# set the ticks / rate
if ticks < 1:
raise ValueError("Expecting the given 'ticks' to be a positive number, but got instead: {}".format(ticks))
self._ticks = ticks
self._cnt = -1
# data from last acquisition
self.data = None
# set the noise
if noise is None:
noise = NoNoise()
if not isinstance(noise, Noise):
raise TypeError("Expecting the given 'noise' to be an instance of Noise, instead got: "
"{}".format(type(noise)))
self._noise = noise
# variable to check if the sensor is enabled
self._enabled = True
# set the latency
if latency is None:
latency = 0
if not isinstance(latency, (int, float)):
raise TypeError("Expecting the given 'latency' to be an int or float, instead got: "
"{}".format(type(latency)))
if latency < 0:
raise ValueError("Expecting the given 'latency' to be a positive number, but got instead: "
"{}".format(latency))
self._latency = latency
self._latent_cnt = -1
# data from last acquisition
self._data = None
self._latent_data = None # self._sense()
##############
# Properties #
@@ -77,28 +130,42 @@ class Sensor(object): # sensor attached to a link or joint
"""Return the simulator instance."""
return self.sim
@property
def position(self):
"""
Return the body's CoM position in the Cartesian world frame.
"""
position = self.sim.get_base_position(self.body_id)
position += self.local_position
return position
# @property
# def position(self):
# """Return the body's base position in the Cartesian world frame."""
# position = self.sim.get_base_position(self.body_id)
# position += self.local_position
# return position
#
# @property
# def orientation(self):
# """Return the body's base orientation in the Cartesian world frame."""
# orientation = self.sim.get_base_orientation(self.body_id)
# orientation = get_quaternion_product(orientation, self.local_orientation)
# return orientation
@property
def orientation(self):
"""
Return the body's CoM orientation in the Cartesian world frame.
"""
orientation = self.sim.get_base_orientation(self.body_id)
orientation = get_quaternion_product(self.local_orientation, orientation)
return orientation
def enabled(self):
"""Return if the sensor is enabled or not."""
return self._enabled
@property
def disabled(self):
"""Return if the sensor is disabled or not."""
return not self._enabled
###########
# Methods #
###########
def reset(self):
"""Reset sensor."""
self._latent_data = self._sense()
def clean(self):
"""clean sensor values."""
pass
def enable(self):
"""Enable the sensor."""
self._enabled = True
@@ -108,18 +175,38 @@ class Sensor(object): # sensor attached to a link or joint
self._enabled = False
@abstractmethod
def _sense(self):
"""Sense method to be implemented in the child class."""
def _sense(self, apply_noise=True):
"""Sense method to be implemented in the child class. This has to apply the noise if you wish to have
noisy data.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
Returns:
np.array: sensed data.
"""
raise NotImplementedError
def sense(self):
"""Get the next sensor value."""
self.cnt += 1
if self.cnt % self.rate == 0:
self.data = self._sense()
self.cnt = 0
return self.data
return self.data
def sense(self, apply_noise=True):
"""Get the next sensor value.
Args:
apply_noise (bool): if we should apply the noise or not. Note that the sensor might already have some noise.
"""
if self._enabled:
self._cnt += 1
if (self._cnt % self._ticks) == 0:
if self._latency == 0: # if no latency
self._data = self._sense()
self._latent_data = self._data
else: # if latency
self._latent_cnt += 1
if (self._latent_cnt % self._latency) == 0:
self._data = self._latent_data
self._latent_data = self._sense(apply_noise=apply_noise)
self._latent_cnt = 0
self._cnt = 0
return self._data
#############
# Operators #
@@ -141,7 +228,8 @@ class Sensor(object): # sensor attached to a link or joint
def __copy__(self):
"""Return a shallow copy of the sensor. This can be overridden in the child class."""
return self.__class__(simulator=self.simulator, body_id=self.body_id, position=self.local_position,
orientation=self.local_orientation, rate=self.rate)
orientation=self.local_orientation, noise=self._noise, ticks=self._ticks,
latency=self._latency)
def __deepcopy__(self, memo={}):
"""Return a deep copy of the sensor. This can be overridden in the child class.
@@ -155,7 +243,8 @@ class Sensor(object): # sensor attached to a link or joint
body_id = copy.deepcopy(self.body_id)
position = copy.deepcopy(self.local_position)
orientation = copy.deepcopy(self.local_orientation)
noise = copy.deepcopy(self._noise)
sensor = self.__class__(simulator=simulator, body_id=body_id, position=position, orientation=orientation,
rate=self.rate)
noise=noise, ticks=self._ticks, latency=self._latency)
memo[self] = sensor
return sensor
+3 -3
View File
@@ -64,16 +64,16 @@ class Walkman(BipedRobot, BiManipulator):
# fovx = 1.3962634rad = 80 degrees, Gaussian noise = N(0, 0.007)
# "left_camera_frame",
self.left_camera = CameraSensor(self.sim, self.id, 11, width=800, height=800, fovy=80, near=0.02, far=300,
rate=30) # 11
ticks=30) # 11
self.right_camera = CameraSensor(self.sim, self.id, 13, width=800, height=800, fovy=80, near=0.02, far=300,
rate=30) # 13
ticks=30) # 13
# Laser (depth) sensor: Hokuyo sensor
# link: "head_hokuyo_frame"
# freq=40, samples = 720, angle = [-1.570796, 1.570796] rad, range = [0.10, 30.0] m
# Gaussian noise: N(0.0, 0.01)
self.depth_camera = CameraSensor(self.sim, self.id, 10, width=200, height=200, fovy=80, near=0.1, far=30.0,
rate=40)
ticks=40)
self.cameras = [self.left_camera, self.right_camera, self.depth_camera]
+2 -2
View File
@@ -1705,8 +1705,8 @@ class Bullet(Simulator):
using forward kinematics.
Returns:
np.array[3]: Cartesian position of CoM
np.array[4]: Cartesian orientation of CoM, in quaternion [x,y,z,w]
np.array[3]: Cartesian world position of CoM
np.array[4]: Cartesian world orientation of CoM, in quaternion [x,y,z,w]
np.array[3]: local position offset of inertial frame (center of mass) expressed in the URDF link frame
np.array[4]: local orientation (quaternion [x,y,z,w]) offset of the inertial frame expressed in URDF link
frame
@@ -0,0 +1,7 @@
Middlewares
===========
This folder provides interfaces to the middlewares that are used in robotics (such as ROS, YARP, etc). All these
classes inherit from the ``Middleware`` abstract class. Middlewares can be provided to simulators which can then use
them to send/receive messages.
@@ -0,0 +1,32 @@
#!/usr/bin/env python
"""Define the abstract middleware API.
Dependencies in PRL:
* NONE
"""
# TODO
import os
import subprocess
import psutil
import signal
import importlib
import inspect
__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 MiddleWare(object):
r"""Middleware (abstract) class
Middlewares can be provided to simulators which can then use them to send/receive messages.
"""
pass
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python
"""Define the ROS middleware API.
Dependencies in PRL:
* `pyrobolearn.simulators.simulator.Simulator`
"""
# TODO
import os
import subprocess
import psutil
import signal
import importlib
import inspect
from pyrobolearn.simulators.simulator import Simulator
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["ROS (Willow Garage)", "Brian Delhaisse"]
__license__ = "GNU GPLv3"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
# TODO: maybe I should inherit from MiddleWare instead of Simulator... Then we can give these MiddleWare to different
# simulators. Other communication middleware layer includes YARP, etc.
class ROS(Simulator):
r"""ROS Interface
"""
def __init__(self, subscribe=False, publish=False, teleoperate=False, master_uri=11311, **kwargs):
super(ROS, self).__init__(render=False)
# Environment variable
self.env = os.environ.copy()
self.env["ROS_MASTER_URI"] = "http://localhost:" + str(master_uri)
# this is for the rospy methods such as: wait_for_service(), init_node(), ...
os.environ['ROS_MASTER_URI'] = self.env['ROS_MASTER_URI']
# run ROS core if not already running
self.roscore = None
if "roscore" not in [p.name() for p in psutil.process_iter()]:
# subprocess.Popen("roscore", env=self.env)
self.roscore = subprocess.Popen(["roscore", "-p", str(master_uri)], env=self.env,
preexec_fn=os.setsid) # , shell=True)
# set variables
self.subscribe = subscribe
self.publish = publish
self.teleoperate = teleoperate
# remember each publisher/subscriber
self.subscribers = {}
self.publishers = {}
self.models = []
self.count_id = -1
def close(self):
"""
Close everything
"""
# delete each subscribers
# delete each publishers
# delete ROS
if self.roscore is not None:
os.killpg(os.getpgid(self.roscore.pid), signal.SIGTERM)
@property
def is_subscribing(self):
"""Return True if we are subscribing to topics."""
return self.subscribe
@property
def is_publishing(self):
"""Return True if we are publishing to topics."""
return self.publish
def load_urdf(self, filename, position=None, orientation=None, use_maximal_coordinates=None,
use_fixed_base=None, flags=None, scale=None):
"""Load the given URDF file.
The load_urdf will send a command to the physics server to load a physics model from a Universal Robot
Description File (URDF). The URDF file is used by the ROS project (Robot Operating System) to describe robots
and other objects, it was created by the WillowGarage and the Open Source Robotics Foundation (OSRF).
Many robots have public URDF files, you can find a description and tutorial here:
http://wiki.ros.org/urdf/Tutorials
Args:
filename (str): a relative or absolute path to the URDF file on the file system of the physics server.
position (vec3): create the base of the object at the specified position in world space coordinates [x,y,z]
orientation (quat): create the base of the object at the specified orientation as world space quaternion
[x,y,z,w]
use_maximal_coordinates (int): Experimental. By default, the joints in the URDF file are created using the
reduced coordinate method: the joints are simulated using the Featherstone Articulated Body algorithm
(btMultiBody in Bullet 2.x). The useMaximalCoordinates option will create a 6 degree of freedom rigid
body for each link, and constraints between those rigid bodies are used to model joints.
use_fixed_base (bool): force the base of the loaded object to be static
flags (int): URDF_USE_INERTIA_FROM_FILE (val=2): by default, Bullet recomputed the inertia tensor based on
mass and volume of the collision shape. If you can provide more accurate inertia tensor, use this flag.
URDF_USE_SELF_COLLISION (val=8): by default, Bullet disables self-collision. This flag let's you
enable it.
You can customize the self-collision behavior using the following flags:
* URDF_USE_SELF_COLLISION_EXCLUDE_PARENT (val=16) will discard self-collision between links that
are directly connected (parent and child).
* URDF_USE_SELF_COLLISION_EXCLUDE_ALL_PARENTS (val=32) will discard self-collisions between a
child link and any of its ancestors (parents, parents of parents, up to the base).
* URDF_USE_IMPLICIT_CYLINDER (val=128), will use a smooth implicit cylinder. By default, Bullet
will tessellate the cylinder into a convex hull.
scale (float): scale factor to the URDF model.
Returns:
int (non-negative): unique id associated to the load model.
"""
id_ = self.count_id
self.count_id += 1
# get path to directory of urdf
path = os.path.dirname(os.path.abspath(filename)) # /path/to/pyrobolearn/robots/urdfs/<robot>/
robot_directory_name = path.split('/')[-1] # <robot>
path = path + '/../../ros/' + robot_directory_name + '/' # /path/to/pyrobolearn/robots/ros/<robot>/
# check if valid robot directory
# if os.path.isdir(path):
robot_path = '/'.join(path.split('/')[-5:-2])
if robot_path == 'pyrobolearn/robots/urdfs':
# get corresponding subscriber/publisher
def check_ros(name, dictionary, id_):
# TODO: do I really need a class for each robot? Can I not just use RobotPublisher?
# if os.path.isfile(path + name + '.py'):
# module = importlib.import_module('pyrobolearn.robots.ros.' + robot_directory_name + '.' + name)
# classes = inspect.getmembers(module, inspect.isclass) # list of (name, class)
# length = len(name)
# robot_name = ''.join(robot_directory_name.split('_'))
#
# # go through each class and get the :attr:`name` corresponding to the robot and add it to the
# # given :attr:`dictionary`
# for name, cls in classes:
# if name[:-length].lower() == robot_name:
# dictionary[id_] = cls(id_=id_)
# break
module = importlib.import_module('pyrobolearn.robots.ros.' + name)
classes = dict(inspect.getmembers(module, inspect.isclass))
cls = classes['Robot' + name.capitalize()]
dictionary[id_] = cls(name=robot_directory_name, id_=id_)
# load subscriber in simulator
if self.subscribe:
check_ros('subscriber', self.subscribers, id_)
# load publisher in simulator
if self.publish:
check_ros('publisher', self.publishers, id_)
return id_
def get_joint_positions(self, body_id, joint_ids):
"""
Get the position of the given joint(s).
Args:
body_id (int): unique body id.
joint_ids (int, list of int): joint id, or list of joint ids.
Returns:
if 1 joint:
float: joint position [rad]
if multiple joints:
np.float[N]: joint positions [rad]
"""
if body_id in self.subscribers:
return self.subscribers[body_id].get_joint_positions(joint_ids)
def set_joint_positions(self, body_id, joint_ids, positions, velocities=None, kps=None, kds=None, forces=None):
"""
Set the position of the given joint(s) (using position control).
Args:
body_id (int): unique body id.
joint_ids (int, list of int): joint id, or list of joint ids.
positions (float, np.float[N]): desired position, or list of desired positions [rad]
velocities (None, float, np.float[N]): desired velocity, or list of desired velocities [rad/s]
kps (None, float, np.float[N]): position gain(s)
kds (None, float, np.float[N]): velocity gain(s)
forces (None, float, np.float[N]): maximum motor force(s)/torque(s) used to reach the target values.
"""
if body_id in self.publishers:
self.publishers[body_id].set_joint_positions(joint_ids, positions)
self.publishers[body_id].publish('joint_states')
def get_joint_velocities(self, body_id, joint_ids):
"""
Get the velocity of the given joint(s).
Args:
body_id (int): unique body id.
joint_ids (int, list of int): joint id, or list of joint ids.
Returns:
if 1 joint:
float: joint velocity [rad/s]
if multiple joints:
np.float[N]: joint velocities [rad/s]
"""
if body_id in self.subscribers:
return self.subscribers[body_id].get_joint_velocities(joint_ids)
def set_joint_velocities(self, body_id, joint_ids, velocities, max_force=None):
"""
Set the velocity of the given joint(s) (using velocity control).
Args:
body_id (int): unique body id.
joint_ids (int, list of int): joint id, or list of joint ids.
velocities (float, np.float[N]): desired velocity, or list of desired velocities [rad/s]
max_force (None, float, np.float[N]): maximum motor forces/torques
"""
if body_id in self.publishers:
self.publishers[body_id].set_joint_velocities(joint_ids, velocities)
self.publishers[body_id].publish('joint_states')
def get_joint_torques(self, body_id, joint_ids):
"""
Get the applied torque(s) on the given joint(s). "This is the motor torque applied during the last `step`.
Note that this only applies in VELOCITY_CONTROL and POSITION_CONTROL. If you use TORQUE_CONTROL then the
applied joint motor torque is exactly what you provide, so there is no need to report it separately." [1]
Args:
body_id (int): unique body id.
joint_ids (int, list of int): a joint id, or list of joint ids.
Returns:
if 1 joint:
float: torque [Nm]
if multiple joints:
np.float[N]: torques associated to the given joints [Nm]
"""
if body_id in self.subscribers:
return self.subscribers[body_id].get_joint_torques(joint_ids)
def set_joint_torques(self, body_id, joint_ids, torques):
"""
Set the torque/force to the given joint(s) (using force/torque control).
Args:
body_id (int): unique body id.
joint_ids (int, list of int): joint id, or list of joint ids.
torques (float, list of float): desired torque(s) to apply to the joint(s) [N].
"""
if body_id in self.publishers:
self.publishers[body_id].set_joint_torques(joint_ids, torques)
self.publishers[body_id].publish('joint_states')
+1 -13
View File
@@ -1,20 +1,8 @@
#!/usr/bin/env python
"""Define the Bullet Simulator API.
This is the main interface that communicates with the PyBullet simulator [1]. By defining this interface, it allows to
decouple the PyRoboLearn framework from the simulator. It also converts some data types to the ones required by
PyBullet. For instance, some methods in PyBullet do not accepts numpy arrays but only lists. The interface provided
here makes the necessary conversions.
The signature of each method defined here are inspired by [1,2] but in accordance with the PEP8 style guide [3].
"""Define the ROS API.
Dependencies in PRL:
* `pyrobolearn.simulators.simulator.Simulator`
References:
[1] PyBullet: https://pybullet.org
[2] PyBullet Quickstart Guide: https://docs.google.com/document/d/10sXEhzFRSnvFcl3XxNGhnD4N2SedqwdAvK3dsihxVUA
[3] PEP8: https://www.python.org/dev/peps/pep-0008/
"""
# TODO
+2 -2
View File
@@ -1142,8 +1142,8 @@ class Simulator(object):
using forward kinematics.
Returns:
np.array[3]: Cartesian position of CoM
np.array[4]: Cartesian orientation of CoM, in quaternion [x,y,z,w]
np.array[3]: Cartesian world position of CoM
np.array[4]: Cartesian world orientation of CoM, in quaternion [x,y,z,w]
np.array[3]: local position offset of inertial frame (center of mass) expressed in the URDF link frame
np.array[4]: local orientation (quaternion [x,y,z,w]) offset of the inertial frame expressed in URDF link
frame
+43 -4
View File
@@ -753,9 +753,38 @@ def get_spatial_transformation_matrix(rotation, position):
quat_converter = QuaternionNumpyConverter(convention=1)
def get_rotated_point_from_quaternion(q, p, convention='xyzw'):
"""
Return the rotated point due to the provided quaternion.
.. math:: P_{\text{rotated}} = q * P * q^{-1} = q * P * q'
where :math:`q` is the unit quaternion (to represents a proper rotation and thus the inverse :math:`q^{-1}` is
equal to the conjugate :math:`q' = \bar{q}`), and :math:`P` is the quaternion where its vector part is equal to
the 3D position of the point :math:`p` and its scalar part is 0.
Args:
q (np.array[4], quaternion.quaternion): quaternion (it doesn't have to be a unit quaternion)
p (np.array[3]): 3d point in space.
convention (str): convention to be adopted when representing the quaternion. You can choose between 'xyzw' or
'wxyz'.
Returns:
np.array[3]: rotated 3 point.
"""
# TODO: fix this!
p_quat = np.array([p[0], p[1], p[2], 0])
q_inv = get_quaternion_inverse(q, convention=convention)
p_rot = get_quaternion_product(q, get_quaternion_product(p_quat, q_inv, convention=convention),
convention=convention)
return p_rot[:3]
def get_quaternion_conjugate(q, convention='xyzw'):
r"""Return the conjugate of the given quaternion; i.e. if the quaternion is given by q = [x,y,z,w] where [x,y,z]
is the vector part and
is the vector part and w is the scalar part, the conjugate is q'=[-x,-y,-z,w].
If the quaternion is a unit quaternion, the conjugate is equal to the inverse of that quaternion.
Args:
q (np.array[4], quaternion.quaternion): quaternion (it doesn't have to be a unit quaternion)
@@ -813,6 +842,13 @@ def get_quaternion_inverse(q, convention='xyzw'):
"""Return the inverse of the given quaternion.
Note: the inverse of a quaternion is the conjugate of the quaternion divided by the square norm of that quaternion.
Thus for a unit quaternion, the inverse of a quaternion is equal to its conjugate.
This is given by:
.. math:: q^{-1} = \frac{\bar{q}}{||q||}
where :math:`\bar{q}` is the conjugate of the quaternion :math:`q`.
Args:
q (np.array[4], quaternion.quaternion): quaternion.
@@ -824,13 +860,13 @@ def get_quaternion_inverse(q, convention='xyzw'):
"""
if isinstance(q, quaternion.quaternion):
return q.inverse()
elif isinstance(q, Iterable):
elif isinstance(q, (np.ndarray, tuple, list)):
if convention == 'xyzw':
x, y, z, w = q
return np.array([-x, -y, -z, w])
return np.array([-x, -y, -z, w]) / np.linalg.norm(q)
elif convention == 'wxyz':
w, x, y, z = q
return np.array([w, -x, -y, -z])
return np.array([w, -x, -y, -z]) / np.linalg.norm(q)
else:
raise NotImplementedError("Asking for a convention that has not been implemented")
else:
@@ -840,6 +876,9 @@ def get_quaternion_inverse(q, convention='xyzw'):
def get_quaternion_product(q1, q2, convention='xyzw'):
"""Return the quaternion product between two quaternions.
The quaternion corresponding to the product :math:`R_1 R_2` where :math:`R_i` are rotation matrices is given by
:math:`q_1 * q_2`.
Args:
q1 (np.array[4], quaternion.quaternion): first quaternion
q2 (np.array[4], quaternion.quaternion): second quaternion