mirror of
https://github.com/wassname/pyrobolearn.git
synced 2026-09-23 13:41:32 +08:00
901 lines
36 KiB
Python
901 lines
36 KiB
Python
#!/usr/bin/env python
|
|
"""Provide the Legged robot abstract classes.
|
|
|
|
Classes that are defined here: LeggedRobot, BipedRobot, QuadrupedRobot, HexapodRobot.
|
|
"""
|
|
|
|
import os
|
|
import collections
|
|
import numpy as np
|
|
from scipy.spatial import ConvexHull
|
|
|
|
from pyrobolearn.robots.robot import Robot
|
|
|
|
__author__ = "Brian Delhaisse"
|
|
__copyright__ = "Copyright 2018, PyRoboLearn"
|
|
__credits__ = ["Brian Delhaisse"]
|
|
__license__ = "GNU GPLv3"
|
|
__version__ = "1.0.0"
|
|
__maintainer__ = "Brian Delhaisse"
|
|
__email__ = "briandelhaisse@gmail.com"
|
|
__status__ = "Development"
|
|
|
|
|
|
class LeggedRobot(Robot):
|
|
r"""Legged robot
|
|
|
|
Legged robots are robots that use some end-effectors to move itself. The movement pattern of these end-effectors
|
|
in the standard regime are rhythmic movements.
|
|
"""
|
|
|
|
def __init__(self, simulator, urdf, position=None, orientation=None, fixed_base=False, scale=1.,
|
|
foot_frictions=None):
|
|
super(LeggedRobot, self).__init__(simulator, urdf, position, orientation, fixed_base, scale=scale)
|
|
|
|
# leg and feet ids
|
|
self.legs = [] # list of legs where a leg is a list of links
|
|
self.feet = [] # list of feet ids
|
|
|
|
# set the foot frictions
|
|
if foot_frictions is not None:
|
|
self.set_foot_friction(foot_frictions)
|
|
|
|
# visual debug
|
|
self.cop_visual = None # visual sphere for center of pressure
|
|
self.zmp_visual = None # visual sphere for zero-moment point
|
|
self.fri_visual = None # visual sphere for foot rotation index
|
|
self.cmp_visual = None # visual sphere for centroidal moment pivot
|
|
|
|
##############
|
|
# Properties #
|
|
##############
|
|
|
|
@property
|
|
def num_legs(self):
|
|
"""Return the number of legs"""
|
|
return len(self.legs)
|
|
|
|
@property
|
|
def num_feet(self):
|
|
"""Return the number of feet; this should normally be equal to the number of legs"""
|
|
return len(self.feet)
|
|
|
|
###########
|
|
# Methods #
|
|
###########
|
|
|
|
def get_leg_ids(self, legs=None):
|
|
"""
|
|
Return the leg id associated with the given leg index(ices)/name(s).
|
|
|
|
Args:
|
|
legs (int, str): leg index(ices) which is [0..num_legs()], or leg name(s)
|
|
|
|
Returns:
|
|
int, list[int]: leg id(s)
|
|
"""
|
|
if legs is not None:
|
|
if isinstance(legs, int):
|
|
return self.legs[legs]
|
|
elif isinstance(legs, str):
|
|
return self.legs[self.get_link_ids(legs)]
|
|
elif isinstance(legs, (list, tuple)):
|
|
leg_ids = []
|
|
for leg in legs:
|
|
if isinstance(leg, int):
|
|
leg_ids.append(self.legs[leg])
|
|
elif isinstance(leg, str):
|
|
leg_ids.append(self.legs[self.get_link_ids(leg)])
|
|
else:
|
|
raise TypeError("Expecting a str or int for items in legs")
|
|
return leg_ids
|
|
return self.legs
|
|
|
|
def get_feet_ids(self, feet=None):
|
|
"""
|
|
Return the foot id associated with the given foot index(ices)/name(s).
|
|
|
|
Args:
|
|
feet (int, str): foot index(ices) which is [0..num_feet()], or foot name(s)
|
|
|
|
Returns:
|
|
int, list[int]: foot id(s)
|
|
"""
|
|
if feet is not None:
|
|
if isinstance(feet, int):
|
|
return self.feet[feet]
|
|
elif isinstance(feet, str):
|
|
return self.feet[self.get_link_ids(feet)]
|
|
elif isinstance(feet, (list, tuple)):
|
|
foot_ids = []
|
|
for foot in feet:
|
|
if isinstance(foot, int):
|
|
foot_ids.append(self.feet[foot])
|
|
elif isinstance(foot, str):
|
|
foot_ids.append(self.feet[self.get_link_ids(foot)])
|
|
else:
|
|
raise TypeError("Expecting a str or int for items in feet")
|
|
return foot_ids
|
|
return self.feet
|
|
|
|
def set_foot_friction(self, frictions, feet_ids=None):
|
|
"""
|
|
Set the foot friction in the simulator.
|
|
|
|
Warnings: only available in the simulator.
|
|
|
|
Args:
|
|
frictions (float, list of float): friction value(s).
|
|
feet_ids (int, list of int): list of foot/feet id(s).
|
|
"""
|
|
if feet_ids is None:
|
|
feet_ids = self.feet
|
|
if isinstance(feet_ids, int):
|
|
feet_ids = [feet_ids]
|
|
if isinstance(frictions, (float, int)):
|
|
frictions = frictions * np.ones(len(feet_ids))
|
|
for foot_id, frict in zip(feet_ids, frictions):
|
|
if isinstance(foot_id, int):
|
|
self.sim.change_dynamics(self.id, foot_id, lateral_friction=frict)
|
|
elif isinstance(foot_id, collections.Iterable):
|
|
for idx in foot_id:
|
|
self.sim.change_dynamics(self.id, idx, lateral_friction=frict)
|
|
else:
|
|
raise TypeError("Expecting foot_id to be a list of int, or an int. Instead got: "
|
|
"{}".format(type(foot_id)))
|
|
|
|
def center_of_pressure(self, floor_id=None):
|
|
r"""
|
|
Compute and return the center of Pressure (CoP).
|
|
|
|
"The CoP is the point on the ground where the resultant of the ground-reaction force acts". [1]
|
|
|
|
This is defined mathematically as:
|
|
|
|
.. math::
|
|
|
|
x_{CoP} = \frac{\sum_i x_i f^i_n}{\sum_i f^i_n}
|
|
y_{CoP} = \frac{\sum_i y_i f^i_n}{\sum_i f^i_n}
|
|
z_{CoP} = \frac{\sum_i z_i f^i_n}{\sum_i f^i_n}
|
|
|
|
where :math:`[x_i, y_i, z_i]` are the coordinates of the contact point :math:`i` on which the normal force
|
|
:math:`f^i_n` acts.
|
|
|
|
Notes:
|
|
- the ZMP and CoP are equivalent for horizontal ground surfaces. For irregular ground surfaces they are
|
|
distinct. [2]
|
|
|
|
Args:
|
|
floor_id (int, None): id of the floor in the simulator. If None, it will use the force/pressure sensors.
|
|
|
|
Returns:
|
|
np.array[3], None: center of pressure. None if the robot is not in contact with the ground.
|
|
|
|
References:
|
|
- [1] "Postural Stability of Biped Robots and Foot-Rotation Index (FRI) Point", Goswami, 1999
|
|
- [2] "Ground Reference Points in Legged Locomotion: Definitions, Biological Trajectories and Control
|
|
Implications", Popovic et al., 2005
|
|
"""
|
|
if floor_id is not None:
|
|
# get contact points between the robot's links and the floor
|
|
points = self.sim.get_contact_points(body1=self.id, body2=floor_id)
|
|
|
|
# if no contact points
|
|
if len(points) == 0:
|
|
return None
|
|
|
|
# compute contact positions (in world frame) and normal force at these points
|
|
positions = np.array([point[6] for point in points]) # contact positions in world frame
|
|
forces = np.array([point[9] for point in points]).reshape(-1, 1) # normal force at contact points
|
|
|
|
# compute CoP and return it
|
|
cop = forces * positions / np.sum(forces)
|
|
cop = np.sum(cop, axis=0)
|
|
|
|
return cop
|
|
|
|
# check if there are force/pressure sensors at the links/joints
|
|
raise NotImplementedError
|
|
|
|
def zero_moment_point(self, update_com=False, floor_id=None):
|
|
r"""
|
|
Zero Moment Point (ZMP).
|
|
|
|
"The ZMP is the point on the ground surface about which the horizontal component of the moment of ground
|
|
reaction force is zero. It resolves the ground reaction force distribution to a single point." [1]
|
|
|
|
Assumptions: the contact area is planar and has sufficiently high friction to keep the feet from sliding.
|
|
|
|
.. math::
|
|
|
|
x_{ZMP} &= x_{CoM} - \frac{F_x}{F_z + Mg} z_{CoM} - \frac{\tau_{y}(\vec{r}_{CoM})}{F_z + Mg} \\
|
|
y_{ZMP} &= y_{CoM} - \frac{F_y}{F_z + Mg} z_{CoM} + \frac{\tau_{x}(\vec{r}_{CoM})}{F_z + Mg}
|
|
|
|
where :math:`[x_{CoM}, y_{CoM}, z_{CoM}]` is the center of mass position, :math:`M` is the body mass,
|
|
:math:`g` is the gravity value, :math:`F = Ma_{CoM}` is the net force acting on the whole body (including the
|
|
gravity force :math:`-Mg`), :math:`\vec{r}_{CoM}` is the body center of mass, and :math:`\tau(\vec{r}_{CoM})`
|
|
is the net whole-body moment about the center of mass.
|
|
|
|
In the case where there are only ground reaction forces (+ the gravity force) acting on the robot, then the
|
|
ZMP point is given by [3]:
|
|
|
|
.. math::
|
|
|
|
x_{ZMP} &= x_{CoM} - \frac{F_{G.R.X}}{F_{G.R.Z}} z_{CoM} - \frac{\tau_{y}(\vec{r}_{CoM})}{F_{G.R.Z}} \\
|
|
y_{ZMP} &= y_{CoM} - \frac{F_{G.R.Y}}{F_{G.R.Z}} z_{CoM} + \frac{\tau_{x}(\vec{r}_{CoM})}{F_{G.R.Z}}
|
|
|
|
where :math:`F_{G.R}` are the ground reaction forces, and the net moment about the CoM
|
|
:math:`\tau(\vec{r}_{CoM})` is computed using the ground reaction forces.
|
|
|
|
The ZMP constraints can be expressed as:
|
|
|
|
.. math::
|
|
|
|
d_x^{-} \leq \frac{n^i_y}{f^i_z} \leq d_x^{+} \\
|
|
d_y^{-} \leq -\frac{n^i_x}{f^i_z} \leq d_y^{+}
|
|
|
|
which ensures the stability of the foot/ground contact. The :math:`(d_x^{-}, d_x^{+})` and
|
|
:math:`(d_y^{-}, d_y^{+})` defines the size of the sole in the x and y directions respectively. Basically,
|
|
this means that the ZMP point must be inside the convex hull in order to have a static stability.
|
|
The :math:`n^i` are the contact spatial torques around the contact point :math:`i`, and :math:`f` is the
|
|
contact spatial force at the contact point :math:`i`.
|
|
|
|
Notes:
|
|
- the ZMP and CoP are equivalent for horizontal ground surfaces. For irregular ground surfaces they are
|
|
distinct. [1]
|
|
- the FRI coincides with the ZMP when the foot is stationary. [1]
|
|
- the CMP coincides with the ZMP, when the moment about the CoM is zero. [1]
|
|
|
|
Args:
|
|
update_com (bool): if True, it will compute and update the CoM position.
|
|
floor_id (int, None): id of the floor in the simulator. If None, it will use the force/pressure sensors.
|
|
|
|
Returns:
|
|
np.array[3], None: zero-moment point. None if the ground reaction force in z is 0.
|
|
|
|
References:
|
|
- [1] "Ground Reference Points in Legged Locomotion: Definitions, Biological Trajectories and Control
|
|
Implications", Popovic et al., 2005
|
|
- [2] "Biped Walking Pattern Generation by using Preview Control of ZMP", Kajita et al., 2003
|
|
- [3] "Exploiting Angular Momentum to Enhance Bipedal Center-of-Mass Control", Hofmann et al., 2009
|
|
"""
|
|
# if we need to update the CoM
|
|
if update_com:
|
|
self.com = self.get_center_of_mass_position()
|
|
|
|
# if the floor id is given, use the simulator to compute the ZMP (using the contact points)
|
|
if floor_id is not None:
|
|
# get contact points between the robot's links and the floor
|
|
points = self.sim.get_contact_points(body1=self.id, body2=floor_id)
|
|
|
|
# if no contact points
|
|
if len(points) == 0:
|
|
return None
|
|
|
|
# compute contact positions in world frame
|
|
positions = np.array([point[6] for point in points])
|
|
|
|
# get all the ground reaction forces
|
|
forces_z = np.array([point[9] * point[7] for point in points]) # normal force
|
|
forces_y = np.array([point[10] * point[11] for point in points]) # first lateral friction force
|
|
forces_x = np.array([point[12] * point[13] for point in points]) # second lateral friction force
|
|
forces = forces_x + forces_y + forces_z # ground reaction forces
|
|
|
|
# compute all the moments with respect to the CoM
|
|
moments = np.cross(positions - self.com, forces)
|
|
|
|
# sum all the ground reaction forces and moments
|
|
forces = np.sum(forces, axis=0)
|
|
moments = np.sum(moments, axis=0)
|
|
|
|
# if no ground reaction forces in z, return None
|
|
if np.isclose(forces[2], 0):
|
|
return None
|
|
|
|
# compute ZMP
|
|
zmp = np.copy(self.com)
|
|
zmp[2] = np.mean(positions, axis=0)[2]
|
|
|
|
zmp[0] += -forces[0]/forces[2] * self.com[2] - moments[1]/forces[2]
|
|
zmp[1] += -forces[1]/forces[2] * self.com[2] + moments[0]/forces[2]
|
|
|
|
# return ZMP
|
|
return zmp
|
|
|
|
# check if there are force/pressure sensors at the links/joints
|
|
raise NotImplementedError
|
|
|
|
def foot_rotation_indicator(self):
|
|
r"""
|
|
Foot Rotation Indicator (FRI).
|
|
|
|
"The FRI is the point (within or outside the support base) where the ground reaction force would have to act
|
|
to keep the foot from accelerating. When the foot is stationary, the FRI coincides with the ZMP." [1]
|
|
|
|
.. math::
|
|
|
|
x_{FRI} &= \frac{x_f \dot{p}^f_z - z_f \dot{p}^f_x - x_{ZMP} F_{G.R.Z} - \dot{L}^f_y(\vec{r}_f)}
|
|
{\dot{p}^f_z - F_{G.R.Z}} \\
|
|
y_{FRI} &= \frac{y_f \dot{p}^F_z - z_f \dot{p}^f_y - y_{ZMP} F_{G.R.Z} - \dot{L}^f_x(\vec{r}_f)}
|
|
{\dot{p}^f_z - F_{G.R.Z}}
|
|
|
|
where :math:`\vec{p}^f` is the linear momentum of the foot's CoM, :math:`F_{G.R}` are the ground reaction
|
|
forces, :math:`[x_f, y_f, z_f]` are the position coordinates of the foot, and :math:`L^f(\vec{r}_f)` is the
|
|
net angular momentum of the foot around the foot.
|
|
|
|
Notes:
|
|
- the FRI coincides with the ZMP when the foot is stationary. [1]
|
|
|
|
References:
|
|
- [1] "Postural Stability of Biped Robots and the Foot-Rotation Indicator (FRI) Point", Goswami, 1999
|
|
- [2] "Ground Reference Points in Legged Locomotion: Definitions, Biological Trajectories and Control
|
|
Implications", Popovic et al., 2005
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def centroidal_moment_pivot(self, update_com=False, floor_id=None):
|
|
r"""
|
|
Centroidal Moment Pivot (CMP).
|
|
|
|
"The CMP is the point where the ground reaction force would have to act to keep the horizontal component of
|
|
the whole-body angular momentum constant. When the moment about the CoM is zero, the CMP coincides with the
|
|
ZMP." [1]
|
|
|
|
.. math::
|
|
|
|
x_{CMP} &= x_{CoM} - \frac{F_{G.R.X}}{F_{G.R.Z}} z_{CoM} \\
|
|
y_{CMP} &= y_{CoM} - \frac{F_{G.R.Y}}{F_{G.R.Z}} z_{CoM}
|
|
|
|
.. math::
|
|
|
|
x_{CMP} &= x_{ZMP} + \frac{\tau_y(\vec{r}_{CoM})}{F_{G.R.Z}} \\
|
|
y_{CMP} &= y_{ZMP} - \frac{\tau_x(\vec{r}_{CoM})}{F_{G.R.Z}}
|
|
|
|
Notes:
|
|
- the CMP coincides with the ZMP, when the moment about the CoM is zero. [1]
|
|
|
|
Args:
|
|
update_com (bool): if True, it will compute and update the CoM position.
|
|
floor_id (int, None): id of the floor in the simulator. If None, it will use the force/pressure sensors.
|
|
|
|
Returns:
|
|
np.array[3], None: centroidal moment pivot point. None if the ground reaction force in z is 0.
|
|
|
|
References:
|
|
- [1] "Ground Reference Points in Legged Locomotion: Definitions, Biological Trajectories and Control
|
|
Implications", Popovic et al., 2005
|
|
"""
|
|
# update the CoM
|
|
if update_com:
|
|
self.get_center_of_mass_position()
|
|
|
|
if floor_id is not None:
|
|
# get contact points between the robot's links and the floor
|
|
points = self.sim.get_contact_points(body1=self.id, body2=floor_id)
|
|
|
|
# if no contact points
|
|
if len(points) == 0:
|
|
return None
|
|
|
|
# compute contact positions in world frame
|
|
positions = np.array([point[6] for point in points])
|
|
|
|
# get all the ground reaction forces
|
|
forces_z = np.array([point[9] * point[7] for point in points]) # normal force
|
|
forces_y = np.array([point[10] * point[11] for point in points]) # first lateral friction force
|
|
forces_x = np.array([point[12] * point[13] for point in points]) # second lateral friction force
|
|
forces = forces_x + forces_y + forces_z # ground reaction forces
|
|
forces = np.sum(forces, axis=0) # sum all the ground reaction forces
|
|
|
|
# if no ground reaction forces in z, return None
|
|
if np.isclose(forces[2], 0):
|
|
return None
|
|
|
|
# compute CMP
|
|
cmp = np.copy(self.com)
|
|
cmp[2] = np.mean(positions, axis=0)[2]
|
|
cmp[0] -= forces[0] / forces[2] * self.com[2]
|
|
cmp[1] -= forces[1] / forces[2] * self.com[2]
|
|
|
|
# return CMP
|
|
return cmp
|
|
|
|
# def divergent_component_motion(self):
|
|
# r"""
|
|
# Divergent Component of Motion, a.k.a 'eXtrapolated Center of Mass'.
|
|
#
|
|
# .. math:: \xi = x + b \dot{x}
|
|
#
|
|
# where :math:`\xi = [\xi_x, \xi_y, \xi_z]` is the DCM point, :math:`x = [x,y,z]` and :math:`\dot{x} = [\dot{x},
|
|
# \dot{y}, \dot{z}]` are the CoM position and velocity, :math:`b > 0` is a time-constant of the DCM dynamics.
|
|
#
|
|
# References:
|
|
# - [1] "Three-dimensional Bipedal Walking Control Based on Divergent Component of Motion", Englsberger et
|
|
# al., 2015
|
|
# """
|
|
# pass
|
|
|
|
# the following methods need to be overwritten in the children classes
|
|
|
|
def move(self, velocity):
|
|
"""Move the robot at the specified velocity."""
|
|
pass
|
|
|
|
def walk_forward(self, speed):
|
|
"""Walk forward."""
|
|
pass
|
|
|
|
def walk_backward(self, speed):
|
|
"""Walk backward."""
|
|
pass
|
|
|
|
def walk_left(self, speed):
|
|
"""Walk sideways to the left."""
|
|
pass
|
|
|
|
def walk_right(self, speed):
|
|
"""Walk sideways to the right."""
|
|
pass
|
|
|
|
def turn_left(self, speed):
|
|
"""Turn left."""
|
|
pass
|
|
|
|
def turn_right(self, speed):
|
|
"""Turn right."""
|
|
pass
|
|
|
|
def draw_support_polygon(self, floor_id, lifetime=1.): # TODO: improve this by remembering the previous hull
|
|
r"""
|
|
draw the support polygon / convex hull in the simulator.
|
|
|
|
Warnings:
|
|
- this is only valid in the simulator.
|
|
- do not call this at a high frequency.
|
|
|
|
Args:
|
|
floor_id (int): id of the floor in the simulator.
|
|
lifetime (float): lifetime of the support polygon before it disappears.
|
|
|
|
References:
|
|
- [1] "A Universal Stability Criterion of the Foot Contact of Legged Robots- Adios ZMP"
|
|
"""
|
|
# get contact points between the robot's links and the floor
|
|
points = self.sim.get_contact_points(body1=self.id, body2=floor_id)
|
|
# points = np.array([point[5] for point in points]) # contact position on robot in Cartesian world coordinates
|
|
points = np.array([point[6] for point in points]) # contact position on floor in Cartesian world coordinates
|
|
|
|
# compute convex hull
|
|
if len(points) > 2: # we need at least 3 points to construct the convex hull
|
|
# compute convex hull
|
|
hull = ConvexHull(points[:, :2])
|
|
vertices = points[hull.vertices] # get the vertices of the convex hull
|
|
|
|
# draw support polygon
|
|
for i in range(len(vertices)):
|
|
self.sim.add_user_debug_line(from_pos=vertices[i-1], to_pos=vertices[i], rgb_color=(0, 1, 0), width=3,
|
|
lifetime=lifetime)
|
|
|
|
# TODO: correct, consider irregular terrain, update visual shape of cones
|
|
def draw_friction_cone(self, floor_id, height=0.2):
|
|
r"""
|
|
Draw the friction cone.
|
|
|
|
The friction cone is defined as:
|
|
|
|
.. math:: C^i_s = {(f^i_x, f^i_y, f^i_z) \in \mathbb{R}^3 | \sqrt{(f^i_x)^2 + (f^i_y)^2} \leq \mu_i f^i_z }
|
|
|
|
where :math:`i` denotes the ith support/contact, :math:`f^i_s` is the contact spatial force exerted at
|
|
the contact point :math:`C_i`, and :math:`\mu_i` is the static friction coefficient at that contact point.
|
|
|
|
"A point contact remains in the fixed contact mode while its contact force f^i lies inside the friction cone"
|
|
[1]. Often, the friction pyramid which is the linear approximation of the friction cone is considered as it
|
|
is easier to manipulate it; e.g. present it as a linear constraint in a quadratic optimization problem.
|
|
|
|
Warnings:
|
|
- this is only valid in the simulator.
|
|
- do not call this at a high frequency.
|
|
|
|
Args:
|
|
floor_id (int): id of the floor in the simulator.
|
|
height (float): maximum height of the cone in the simulator.
|
|
|
|
References:
|
|
- [1] https://scaron.info/teaching/friction-cones.html
|
|
- [2] "Stability of Surface Contacts for Humanoid Robots: Closed-Form Formulae of the Contact Wrench Cone
|
|
for Rectangular Support Areas", Caron et al., 2015
|
|
"""
|
|
filename = os.path.dirname(__file__) + '/../worlds/meshes/cone.obj'
|
|
|
|
# get contact points between the robot's links and the floor
|
|
points = self.sim.get_contact_points(body1=self.id, body2=floor_id)
|
|
mu = self.sim.get_dynamics_info(floor_id)[1] # friction coefficient
|
|
|
|
ids = []
|
|
for point in points:
|
|
position = point[6] # contact position on floor in Cartesian world coordinates
|
|
fz_dir = point[7] # contact normal on floor pointing towards the robot
|
|
fz = point[9] # normal force applied during the last step
|
|
fy = point[10] # lateral friction force in the first lateral friction direction
|
|
fy_dir = point[11] # first lateral friction direction
|
|
fx = point[12] # lateral friction force in the second lateral friction direction
|
|
fx_dir = point[13] # second lateral friction direction
|
|
|
|
# make sure that fz is bigger than 0
|
|
if not np.allclose(fz, 0):
|
|
|
|
# rescale fx, fy, fz
|
|
# TODO uncomment the original calculations
|
|
fx = height # np.abs(fx / (mu*fz)) * height
|
|
fy = height # np.abs(fy / (mu*fz)) * height
|
|
fz = height
|
|
|
|
position += np.array([0., 0., height * 0.5])
|
|
id_ = self.sim.load_mesh(filename, position, orientation=(0, 1, 0, 0), mass=0.,
|
|
scale=(fx, fy, fz), color=(0.5, 0., 0., 0.5), with_collision=False)
|
|
ids.append(id_)
|
|
|
|
return ids
|
|
|
|
# TODO: add pyramid 3D object, consider irregular terrains, update pyramid visual shape
|
|
def draw_friction_pyramid(self, floor_id, height=0.2):
|
|
r"""
|
|
Draw friction pyramid.
|
|
|
|
The friction pyramid is defined as:
|
|
|
|
.. math:: P^i_s = {(f^i_x, f^i_y, f^i_z) \in \mathbb{R}^3 | f^i_x \leq \mu_i f^i_z, f^i_y \leq \mu_i f^i_z}
|
|
|
|
where where :math:`i` denotes the ith support/contact, :math:`f^i_s` is the contact spatial force exerted at
|
|
the contact point :math:`C_i`, and :math:`\mu_i` is the static friction coefficient at that contact point.
|
|
If the static friction coefficient is given by :math:`\frac{\mu_i}{\sqrt{2}}`, then we are making an inner
|
|
approximation (i.e. the pyramid is inside the cone) instead of an outer approximation (i.e. the cone is inside
|
|
the pyramid). [1]
|
|
|
|
This linear approximation is often used as a linear constraint in a quadratic optimization problem along with
|
|
the unilateral constraint :math:`f^i_z \geq 0`.
|
|
|
|
Warnings:
|
|
- this is only valid in the simulator.
|
|
- do not call this at a high frequency.
|
|
|
|
Args:
|
|
floor_id (int): id of the floor in the simulator.
|
|
height (float): maximum height of the pyramid in the simulator.
|
|
|
|
References:
|
|
- [1] https://scaron.info/teaching/friction-cones.html
|
|
- [2] "Stability of Surface Contacts for Humanoid Robots: Closed-Form Formulae of the Contact Wrench Cone
|
|
for Rectangular Support Areas", Caron et al., 2015
|
|
"""
|
|
filename = os.path.dirname(__file__) + '/../worlds/meshes/pyramid.obj'
|
|
|
|
# get contact points between the robot's links and the floor
|
|
points = self.sim.get_contact_points(body1=self.id, body2=floor_id)
|
|
mu = self.sim.get_dynamics_info(floor_id)[1] # friction coefficient
|
|
|
|
ids = []
|
|
for point in points:
|
|
position = point[6] # contact position on floor in Cartesian world coordinates
|
|
fz_dir = point[7] # contact normal on floor pointing towards the robot
|
|
fz = point[9] # normal force applied during the last step
|
|
fy = point[10] # lateral friction force in the first lateral friction direction
|
|
fy_dir = point[11] # first lateral friction direction
|
|
fx = point[12] # lateral friction force in the second lateral friction direction
|
|
fx_dir = point[13] # second lateral friction direction
|
|
|
|
# make sure that fz is bigger than 0
|
|
if not np.allclose(fz, 0):
|
|
# rescale fx, fy, fz
|
|
# TODO uncomment the original calculations
|
|
fx = height # np.abs(fx / (mu*fz)) * height
|
|
fy = height # np.abs(fy / (mu*fz)) * height
|
|
fz = height
|
|
|
|
position += np.array([0., 0., height * 0.5])
|
|
id_ = self.sim.load_mesh(filename, position, orientation=(0, 1, 0, 0), mass=0.,
|
|
scale=(fx, fy, fz), color=(0.5, 0., 0., 0.5), with_collision=False)
|
|
ids.append(id_)
|
|
|
|
return ids
|
|
|
|
def draw_cop(self, cop=None, radius=0.05, color=(0, 1, 0, 0.8)):
|
|
"""
|
|
Draw the CoP in the simulator.
|
|
|
|
Args:
|
|
cop (np.array[3], None, int): center of pressure. If None or int, it will compute the CoP. If None, it
|
|
will compute it using the force sensors. If int, it will be assumed to be the floor's id, and will
|
|
use the simulator to compute the CoP.
|
|
radius (float): radius of the sphere representing the CoP of the robot
|
|
color (tuple of 4 floats): rgba color of the sphere (each value is between 0 and 1). By default it is red.
|
|
"""
|
|
if cop is None or isinstance(cop, (int, long)):
|
|
cop = self.center_of_pressure(floor_id=cop)
|
|
if self.cop_visual is None and cop is not None: # create visual shape if not already created
|
|
cop_visual_shape = self.sim.create_visual_shape(self.sim.GEOM_SPHERE, radius=radius, rgba_color=color)
|
|
self.cop_visual = self.sim.create_body(mass=0, visual_shape_id=cop_visual_shape, position=cop)
|
|
else: # set CoP position
|
|
if cop is None:
|
|
self.remove_cop()
|
|
else:
|
|
self.sim.reset_base_pose(self.cop_visual, cop, [0, 0, 0, 1])
|
|
|
|
def draw_zmp(self, zmp=None, radius=0.05, color=(1, 1, 0, 0.8), update_com=False):
|
|
"""
|
|
Draw the ZMP in the simulator.
|
|
|
|
Args:
|
|
zmp (np.array[3], None, int): zero-moment point. If None or int, it will compute the ZMP. If None, it
|
|
will compute it using the force sensors. If int, it will be assumed to be the floor's id, and will
|
|
use the simulator to compute the ZMP.
|
|
radius (float): radius of the sphere representing the ZMP of the robot
|
|
color (float[4]): rgba color of the sphere (each value is between 0 and 1). By default it is red.
|
|
update_com (bool): if we should compute the CoM, if None or int is given for the :attr:`zmp`.
|
|
"""
|
|
if zmp is None or isinstance(zmp, (int, long)):
|
|
zmp = self.zero_moment_point(update_com=update_com, floor_id=zmp)
|
|
if self.zmp_visual is None and zmp is not None: # create visual shape if not already created
|
|
zmp_visual_shape = self.sim.create_visual_shape(self.sim.GEOM_SPHERE, radius=radius, rgba_color=color)
|
|
self.zmp_visual = self.sim.create_body(mass=0, visual_shape_id=zmp_visual_shape, position=zmp)
|
|
else: # set ZMP position
|
|
if zmp is None:
|
|
self.remove_zmp()
|
|
else:
|
|
self.sim.reset_base_pose(self.zmp_visual, zmp, [0, 0, 0, 1])
|
|
|
|
def draw_cmp(self, cmp=None, radius=0.05, color=(1, 0, 0, 0.8), update_com=False):
|
|
"""
|
|
Draw the CMP in the simulator.
|
|
|
|
Args:
|
|
cmp (np.array[3], None, int): central moment pivot. If None or int, it will compute the CMP. If None, it
|
|
will compute it using the force sensors. If int, it will be assumed to be the floor's id, and will
|
|
use the simulator to compute the CMP.
|
|
radius (float): radius of the sphere representing the CMP of the robot
|
|
color (float[4]): rgba color of the sphere (each value is between 0 and 1). By default it is red.
|
|
update_com (bool): if we should compute the CoM, if None or int is given for the :attr:`cmp`.
|
|
"""
|
|
if cmp is None or isinstance(cmp, (int, long)):
|
|
cmp = self.centroidal_moment_pivot(update_com=update_com, floor_id=cmp)
|
|
if self.cmp_visual is None and cmp is not None: # create visual shape if not already created
|
|
cmp_visual_shape = self.sim.create_visual_shape(self.sim.GEOM_SPHERE, radius=radius, rgba_color=color)
|
|
self.cmp_visual = self.sim.create_body(mass=0, visual_shape_id=cmp_visual_shape, position=cmp)
|
|
else: # set ZMP position
|
|
if cmp is None:
|
|
self.remove_cmp()
|
|
else:
|
|
self.sim.reset_base_pose(self.cmp_visual, cmp, [0, 0, 0, 1])
|
|
|
|
def draw_fri(self, fri=None, radius=0.05, color=(1, 0, 0, 0.8), update_com=False):
|
|
"""
|
|
Draw the FRI in the simulator.
|
|
|
|
Args:
|
|
fri (np.array[3], None, int): central moment pivot. If None or int, it will compute the FRI. If None, it
|
|
will compute it using the force sensors. If int, it will be assumed to be the floor's id, and will
|
|
use the simulator to compute the FRI.
|
|
radius (float): radius of the sphere representing the FRI of the robot
|
|
color (float[4]): rgba color of the sphere (each value is between 0 and 1). By default it is red.
|
|
update_com (bool): if we should compute the CoM, if None or int is given for the :attr:`fri`.
|
|
"""
|
|
if fri is None or isinstance(fri, (int, long)):
|
|
fri = self.centroidal_moment_pivot(update_com=update_com, floor_id=fri)
|
|
if self.fri_visual is None and fri is not None: # create visual shape if not already created
|
|
fri_visual_shape = self.sim.create_visual_shape(self.sim.GEOM_SPHERE, radius=radius, rgba_color=color)
|
|
self.fri_visual = self.sim.create_body(mass=0, visual_shape_id=fri_visual_shape, position=fri)
|
|
else: # set FRI position
|
|
if fri is None:
|
|
self.remove_cmp()
|
|
else:
|
|
self.sim.reset_base_pose(self.fri_visual, fri, [0, 0, 0, 1])
|
|
|
|
def remove_cop(self):
|
|
"""
|
|
Remove the CoP from the simulator.
|
|
"""
|
|
if self.cop_visual is not None:
|
|
self.sim.remove_body(self.cop_visual)
|
|
self.cop_visual = None
|
|
|
|
def remove_zmp(self):
|
|
"""
|
|
Remove the ZMP from the simulator.
|
|
"""
|
|
if self.zmp_visual is not None:
|
|
self.sim.remove_body(self.zmp_visual)
|
|
self.zmp_visual = None
|
|
|
|
def remove_cmp(self):
|
|
"""
|
|
Remove the CMP from the simulator.
|
|
"""
|
|
if self.cmp_visual is not None:
|
|
self.sim.remove_body(self.cmp_visual)
|
|
self.cmp_visual = None
|
|
|
|
def remove_fri(self):
|
|
"""
|
|
Remove the FRI from the simulator.
|
|
"""
|
|
if self.fri_visual is not None:
|
|
self.sim.remove_body(self.fri_visual)
|
|
self.fri_visual = None
|
|
|
|
|
|
class BipedRobot(LeggedRobot):
|
|
r"""Biped Robot
|
|
|
|
A biped robot is a robot which has 2 legs.
|
|
"""
|
|
|
|
def __init__(self, simulator, urdf, position=None, orientation=None, fixed_base=False, scale=1.):
|
|
super(BipedRobot, self).__init__(simulator, urdf, position, orientation, fixed_base, scale)
|
|
|
|
self.left_leg_id = 0
|
|
self.right_leg_id = 1
|
|
|
|
##############
|
|
# Properties #
|
|
##############
|
|
|
|
@property
|
|
def left_leg(self):
|
|
"""Return the left leg joint ids"""
|
|
return self.legs[self.left_leg_id]
|
|
|
|
@property
|
|
def right_leg(self):
|
|
"""Return the right leg joint ids"""
|
|
return self.legs[self.right_leg_id]
|
|
|
|
@property
|
|
def left_foot(self):
|
|
"""Return the left foot id"""
|
|
return self.feet[self.left_leg_id]
|
|
|
|
@property
|
|
def right_foot(self):
|
|
"""Return the right foot id"""
|
|
return self.feet[self.right_leg_id]
|
|
|
|
|
|
class QuadrupedRobot(LeggedRobot):
|
|
r"""Quadruped robot
|
|
|
|
A quadruped robot is a robot which has 4 legs.
|
|
"""
|
|
|
|
def __init__(self, simulator, urdf, position=None, orientation=None, fixed_base=False, scale=1.):
|
|
super(QuadrupedRobot, self).__init__(simulator, urdf, position, orientation, fixed_base, scale)
|
|
|
|
self.left_front_leg_id = 0
|
|
self.right_front_leg_id = 1
|
|
self.left_back_leg_id = 2
|
|
self.right_back_leg_id = 3
|
|
|
|
##############
|
|
# Properties #
|
|
##############
|
|
|
|
@property
|
|
def left_front_leg(self):
|
|
"""Return the left front leg joint ids"""
|
|
return self.legs[self.left_front_leg_id]
|
|
|
|
@property
|
|
def right_front_leg(self):
|
|
"""Return the right front leg joint ids"""
|
|
return self.legs[self.right_front_leg_id]
|
|
|
|
@property
|
|
def left_back_leg(self):
|
|
"""Return the left back leg joint ids"""
|
|
return self.legs[self.left_back_leg_id]
|
|
|
|
@property
|
|
def right_back_leg(self):
|
|
"""Return the right back leg joint ids"""
|
|
return self.legs[self.right_back_leg_id]
|
|
|
|
@property
|
|
def left_front_foot(self):
|
|
"""Return the left front foot id"""
|
|
return self.feet[self.left_front_leg_id]
|
|
|
|
@property
|
|
def right_front_foot(self):
|
|
"""Return the right front foot id"""
|
|
return self.feet[self.right_front_leg_id]
|
|
|
|
@property
|
|
def left_back_foot(self):
|
|
"""Return the left back foot id"""
|
|
return self.feet[self.left_back_leg_id]
|
|
|
|
@property
|
|
def right_back_foot(self):
|
|
"""Return the right back foot id"""
|
|
return self.feet[self.right_back_leg_id]
|
|
|
|
|
|
class HexapodRobot(LeggedRobot):
|
|
r"""Hexapod Robot
|
|
|
|
An hexapod robot is a robot which has 6 legs.
|
|
"""
|
|
|
|
def __init__(self, simulator, urdf, position, orientation=None, fixed_base=False, scale=1.):
|
|
super(HexapodRobot, self).__init__(simulator, urdf, position, orientation, fixed_base, scale)
|
|
|
|
self.left_front_leg_id = 0
|
|
self.right_front_leg_id = 1
|
|
self.left_middle_leg_id = 2
|
|
self.right_middle_leg_id = 3
|
|
self.left_back_leg_id = 4
|
|
self.right_back_leg_id = 5
|
|
|
|
##############
|
|
# Properties #
|
|
##############
|
|
|
|
@property
|
|
def left_front_leg(self):
|
|
"""Return the left front leg ids"""
|
|
return self.legs[self.left_front_leg_id]
|
|
|
|
@property
|
|
def right_front_leg(self):
|
|
"""Return the right front leg ids"""
|
|
return self.legs[self.right_front_leg_id]
|
|
|
|
@property
|
|
def left_middle_leg(self):
|
|
"""Return the left middle leg ids"""
|
|
return self.legs[self.left_middle_leg_id]
|
|
|
|
@property
|
|
def right_middle_leg(self):
|
|
"""Return the right middle leg ids"""
|
|
return self.legs[self.right_middle_leg_id]
|
|
|
|
@property
|
|
def left_back_leg(self):
|
|
"""Return the left back leg ids"""
|
|
return self.legs[self.left_back_leg_id]
|
|
|
|
@property
|
|
def right_back_leg(self):
|
|
"""Return the right back leg ids"""
|
|
return self.legs[self.right_back_leg_id]
|
|
|
|
@property
|
|
def left_front_foot(self):
|
|
"""Return the left front foot id"""
|
|
return self.feet[self.left_front_leg_id]
|
|
|
|
@property
|
|
def right_front_foot(self):
|
|
"""Return the right front foot id"""
|
|
return self.feet[self.right_front_leg_id]
|
|
|
|
@property
|
|
def left_middle_foot(self):
|
|
"""Return the left middle foot id"""
|
|
return self.feet[self.left_middle_leg_id]
|
|
|
|
@property
|
|
def right_middle_foot(self):
|
|
"""Return the right middle foot id"""
|
|
return self.feet[self.right_middle_leg_id]
|
|
|
|
@property
|
|
def left_back_foot(self):
|
|
"""Return the left back foot id"""
|
|
return self.feet[self.left_back_leg_id]
|
|
|
|
@property
|
|
def right_back_foot(self):
|
|
"""Return the right back foot id"""
|
|
return self.feet[self.right_back_leg_id]
|