Differentiable Features & Collision Evaluation
The 1st tutorial introduced the basic concepts of features. The lecture script section ‘Kinematics’ introduces this a bit more formally.
This tutorial first gives an overview over available features, and then provides more details specifically on collision features.
Recap of evaluating features directly
Let’s first recap how features are directly computed on a configuration - as introduced in the 1st tutorial:
[1]:
from robotic import ry
[2]:
C = ry.Config()
C.addFile(ry.raiPath('panda/panda.g'))
q = C.getJointState()
[3]:
[y,J] = C.eval(ry.FS.position, ['gripper'])
print('feature value:', y, '\nJacobian:', J)
feature value: [2.89890247e-01 1.25455202e-16 8.05237266e-01]
Jacobian: [[-1.25455202e-16 4.72237266e-01 -1.15172364e-16 -2.32080381e-01
0.00000000e+00 4.48170606e-02 0.00000000e+00 0.00000000e+00]
[ 2.89890247e-01 6.43685653e-17 5.54002326e-01 2.83784184e-16
1.63424512e-01 2.26026356e-16 -1.38777878e-17 0.00000000e+00]
[ 0.00000000e+00 -2.89890247e-01 -1.79370329e-16 5.11220138e-01
0.00000000e+00 2.32670220e-01 0.00000000e+00 0.00000000e+00]]
[4]:
# the x-axis of the given frame in world coordinates
C.eval(ry.FS.vectorX, ['gripper'])
[4]:
(array([ 0.38205142, -0.70710678, 0.59500984]),
array([[ 7.07106781e-01, 5.95009840e-01, 3.82051424e-01,
-5.95009840e-01, 3.82051424e-01, -5.95009840e-01,
-3.82051424e-01, 0.00000000e+00],
[ 3.82051424e-01, 8.48324576e-17, 7.07106781e-01,
2.12081144e-16, -2.94260250e-01, 3.71142002e-16,
-7.07106781e-01, 0.00000000e+00],
[ 0.00000000e+00, -3.82051424e-01, 5.95009840e-01,
3.82051424e-01, -5.95009840e-01, 3.82051424e-01,
-5.95009840e-01, 0.00000000e+00]]))
The signature of the eval
method is
The feature symbol (
FS.<name>
in python;FS_<name>
in cpp)The set of frames it refers to, given as list of frame names
Optionally: A scale, that can also be a matrix to down-project a feature (see below)
Optionally: A target, which changes the zero-point of the features (see below)
List of features
Here is a full list of feature symbols:
[5]:
ry.FS.__members__.keys()
[5]:
dict_keys(['position', 'positionDiff', 'positionRel', 'quaternion', 'quaternionDiff', 'quaternionRel', 'pose', 'poseDiff', 'poseRel', 'vectorX', 'vectorXDiff', 'vectorXRel', 'vectorY', 'vectorYDiff', 'vectorYRel', 'vectorZ', 'vectorZDiff', 'vectorZRel', 'scalarProductXX', 'scalarProductXY', 'scalarProductXZ', 'scalarProductYX', 'scalarProductYY', 'scalarProductYZ', 'scalarProductZZ', 'gazeAt', 'angularVel', 'accumulatedCollisions', 'jointLimits', 'distance', 'negDistance', 'oppose', 'qItself', 'jointState', 'aboveBox', 'insideBox', 'pairCollision_negScalar', 'pairCollision_vector', 'pairCollision_normal', 'pairCollision_p1', 'pairCollision_p2', 'standingAbove', 'physics', 'contactConstraints', 'energy', 'transAccelerations', 'transVelocities'])
But some of these (esp later ones) are exotic or experimental. The core pre-defined features are the following:
FS |
frames |
D |
description |
---|---|---|---|
position |
[A] |
3 |
3D position of A in world coordinates |
positionDiff |
[A,B] |
3 |
difference of 3D positions of A and B in world coordinates |
positionRel |
[A,B] |
3 |
3D position of A in B-coordinates |
quaternion |
[A] |
4 |
4D quaternion of A in world coordinatesfootnote[There is ways to handle the invariance w.r.t. quaternion sign properly.] |
quaternionDiff |
[A,B] |
4 |
… |
quaternionRel |
[A,B] |
4 |
… |
pose |
[A] |
7 |
7D pose of A in world coordinates |
poseDiff |
[A,B] |
7 |
… |
poseRel |
[A,B] |
7 |
… |
vectorX |
[A] |
3 |
The x-basis-vector of frame A in world coordinates |
vectorXDiff |
[A,B] |
3 |
The difference of the above for two frames A and B |
vectorXRel |
[A,B] |
3 |
The x-basis-vector of frame A in B-coordinates |
vectorY… |
same as above |
||
scalarProductXX |
[A,B] |
1 |
The scalar product of the x-basis-vector of frame A with the x-basis-vector of frame B |
scalarProduct… |
[A,B] |
as above |
|
angularVel |
[A] |
3 |
The angular velocity of frame A across two configurations (must be order=1!) |
accumulatedCollisions |
[] |
1 |
The sum of collision penetrations; when negative/zero, nothing is colliding |
jointLimits |
[] |
1 |
The sum of joint limit penetrations; when negative/zero, all joint limits are ok |
negDistance |
[A,B] |
1 |
The NEGATIVE distance between convex meshes A and B, positive for penetration |
qItself |
[] |
n |
The configuration joint vector |
aboveBox |
[A,B] |
4 |
when all negative, A is above (inside support of) the box B |
insideBox |
[A,B] |
6 |
when all negative, A is inside the box B |
Position features
Let’s briefly clarify the difference between position
, positionDiff
, and positionRel
:
position
is the position of a frameA
in world coordinatespositionDiff
is the difference (in world coordinates) of positionB
MINUS positionA
positionRel
is the position of frameA
in the RELATIVE coordinates of frameB
The best example is if A
is a camera: Assume you would like to frame B
to be positioned exactly at coordinate (0,0,-.3) in the coordinate frame of A
– meaning exactly 30cm centrally in front of the camera – then the following feature would evaluate the error:
[6]:
C.eval(ry.FS.positionRel, ['gripper', 'panda_joint1'], scale=[1], target=[0, 0, .3])
[6]:
(array([2.89890247e-01, 1.25455202e-16, 1.72237266e-01]),
array([[ 0.00000000e+00, 4.72237266e-01, -1.15172364e-16,
-2.32080381e-01, 0.00000000e+00, 4.48170606e-02,
0.00000000e+00, 0.00000000e+00],
[ 0.00000000e+00, 6.43685653e-17, 5.54002326e-01,
2.83784184e-16, 1.63424512e-01, 2.26026356e-16,
-1.38777878e-17, 0.00000000e+00],
[ 0.00000000e+00, -2.89890247e-01, -1.79370329e-16,
5.11220138e-01, 0.00000000e+00, 2.32670220e-01,
0.00000000e+00, 0.00000000e+00]]))
Concretely, if you minimize the above feature, the gripper
will look exactly(=centrally) at panda_joint1
with 30cm distance.
Scalar product features
The scalarProduct
features are also very useful to define relative between frames (e.g. for grasping). For instance, (FS.scalarProductXX, {'handL', 'handR'}, target=[1])
says that the scalar product of the x-axes (e.g. directions of the index finger) of both hands should equal 1, which means they are aligned. And
(FS.scalarProductXY, {'handL', 'handR'})
(FS.scalarProductXZ, {'handL', 'handR'})
says that the the x-axis of handL should be orthogonal (zero scalar product) to the y- and z-axis of handR. So this also describes aligning both x-axes. However, this formulation is much more robust, as it has good error gradients around the optimum.
Scale and target transformation
Let’s explain the scale
and target
in detail: Formally, specifying a target and scale redefines a feature to become
The target needs to be a \(D\)-dim vector and defines the zero-point of the feature.
The scale can be
a scalar (just a factor),
a \(D\)-vector (multiplying element-wise to the feature)
or a matrix.
We can do interesting things when scale
is a matrix. For instance, if we only want the \(xy\)-position of a frame returned, we can choose a matrix \(S=\begin{pmatrix}1 & 0 & 0 \\ 0 & 1 & 0\end{pmatrix}\), which multiplies to the 3D feature to return a 2D feature. In code:
[7]:
C.eval(ry.FS.position, ['gripper'], [[1,0,0],[0,1,0]])
[7]:
(array([2.89890247e-01, 1.25455202e-16]),
array([[-1.25455202e-16, 4.72237266e-01, -1.15172364e-16,
-2.32080381e-01, 0.00000000e+00, 4.48170606e-02,
0.00000000e+00, 0.00000000e+00],
[ 2.89890247e-01, 6.43685653e-17, 5.54002326e-01,
2.83784184e-16, 1.63424512e-01, 2.26026356e-16,
-1.38777878e-17, 0.00000000e+00]]))
A very useful application is again if we care about a camera looking at a point: If we want frame B
to appear centrally in the \(xy\)-plane of frame A
, we can mimimize the feature:
[8]:
C.eval(ry.FS.positionRel, ['gripper', 'panda_joint1'], [[1,0,0],[0,1,0]])
[8]:
(array([2.89890247e-01, 1.25455202e-16]),
array([[ 0.00000000e+00, 4.72237266e-01, -1.15172364e-16,
-2.32080381e-01, 0.00000000e+00, 4.48170606e-02,
0.00000000e+00, 0.00000000e+00],
[ 0.00000000e+00, 6.43685653e-17, 5.54002326e-01,
2.83784184e-16, 1.63424512e-01, 2.26026356e-16,
-1.38777878e-17, 0.00000000e+00]]))
Collision features
Let’s evaluate the accumulative collision scalar and its Jacobian
[9]:
coll = C.feature(ry.FS.accumulatedCollisions, [])
C.computeCollisions() #collisions/proxies are not automatically computed on set...State
coll.eval(C)
[9]:
(array([0.]), array([[0., 0., 0., 0., 0., 0., 0., 0.]]))
Let’s move into collision and redo this
[10]:
from robotic import ry
C = ry.Config()
C.clear()
C.addFile(ry.raiPath('panda/panda.g'))
C.addFile(ry.raiPath('panda/panda.g'), 'r_')
base_r = C.getFrame('r_panda_base')
base_r.setPosition([.0, .5, .0])
[10]:
<robotic.ry.Frame at 0x7fa7b82d3ab0>
[11]:
C.selectJoints(['panda_joint1', 'panda_joint2', 'r_panda_joint1', 'r_panda_joint2'])
C.setJointState([1.,-.8,-1.,-.8])
C.view()
[11]:
0
The configuration is now in collision. We can evaluate between some specific shape-pairs that show this:
[12]:
C.eval(ry.FS.negDistance, ['palm', 'r_palm'])
[12]:
(array([0.13976684]),
array([[ 0.2021495 , -0.06471866, 0.00841045, 0.55724234]]))
[13]:
C.eval(ry.FS.negDistance, ['panda_coll7', 'r_panda_coll7'])
[13]:
(array([0.08287262]),
array([[ 0.14218262, 0.43479204, -0.14218262, 0.43479204]]))
[14]:
C.eval(ry.FS.negDistance, ['panda_coll6', 'r_panda_coll6'])
[14]:
(array([-0.05097286]),
array([[-0.05482199, 0.48443921, -0.12142734, 0.29574326]]))
However, the above features only return penetration (=negDistance) between specific pairs of shapes. For holistic collision checking we query if any pair of shapes collides. This is called broad phase collision checking. The following does this (calling fcl
internally):
[15]:
C.computeCollisions()
C.getCollisions()
[15]:
[('r_palm', 'palm', -0.13976683583198263),
('r_finger1', 'palm', 0.013576748081966544),
('r_finger2', 'palm', 0.014228713904190754),
('r_panda_coll6', 'palm', -0.015404140037349565),
('r_panda_coll5', 'palm', 0.04525630035427913),
('r_finger2', 'r_panda_coll5', 0.13749756426853948),
('r_panda_coll7', 'palm', -0.09280264001977123),
('r_panda_coll2', 'r_panda_coll5', 0.1945494196968938),
('r_panda_coll3', 'palm', 0.36007247820155464),
('r_palm', 'r_panda_coll3', 0.35692722195160376),
('r_panda_coll7', 'r_panda_coll4', 0.32362431478923487),
('panda_coll2', 'panda_coll5', 0.19454941969689368),
('panda_coll7', 'panda_coll4', 0.32362431478923515),
('r_panda_coll2', 'panda_coll0', 0.28717304075316374),
('r_panda_coll0', 'panda_coll1', 0.27280115353885176),
('r_panda_coll1', 'panda_coll0', 0.2728011535388518),
('r_panda_coll0', 'panda_coll0', 0.19999999999999998),
('r_panda_coll0b', 'panda_coll1', 0.22160801784824963),
('r_panda_coll0b', 'panda_coll0', 0.13131351929875237),
('r_panda_coll0', 'panda_coll2', 0.28717304075316374),
('r_panda_coll0b', 'panda_coll2', 0.2638941651174542),
('r_panda_coll6', 'panda_coll6', 0.050972864653827),
('r_panda_coll6', 'panda_coll7', 0.015315130184475917),
('r_panda_coll6', 'finger1', 0.05202028460982694),
('r_panda_coll6', 'finger2', 0.03488780676276168),
('r_panda_coll5', 'panda_coll6', 0.11164377353037033),
('r_panda_coll5', 'panda_coll7', 0.08766336859618301),
('r_panda_coll5', 'finger1', 0.12895455840607237),
('r_panda_coll5', 'finger2', 0.06147141318949159),
('r_panda_coll7', 'panda_coll6', 0.013907106655053708),
('r_panda_coll7', 'panda_coll7', -0.08287262318906746),
('r_panda_coll7', 'finger1', 0.030647107437714566),
('r_panda_coll7', 'finger2', 0.03284520763356755),
('r_palm', 'panda_coll6', -0.05737613444861561),
('r_finger1', 'panda_coll6', -0.020651551158550177),
('r_finger2', 'panda_coll6', 0.112620748583009),
('r_palm', 'panda_coll7', -0.1273471133054103),
('r_finger1', 'panda_coll7', 0.0011556581521950104),
('r_finger2', 'panda_coll7', 0.05638875573522109),
('r_palm', 'finger1', -0.02159299051272197),
('r_palm', 'finger2', -0.014663617396226986),
('r_finger2', 'finger1', 0.03712381290870001),
('r_finger2', 'finger2', 0.03836716448878992),
('palm', 'panda_coll3', 0.3569272219516041),
('r_palm', 'panda_coll3', 0.3884474823677896),
('r_panda_coll7', 'panda_coll5', 0.08262699740729021),
('r_palm', 'panda_coll5', 0.0151854277723599),
('r_finger1', 'panda_coll5', 0.037025559294402516),
('r_finger2', 'panda_coll5', 0.16135689397161754)]
[16]:
C.getTotalPenetration()
[16]:
0.572477645899696
The last command is useful for what is classically called binary collision check: if totalPenetration
is zero, we have no collisions.
Finally, we can get the same information in a differentiable manner, as a feature:
[17]:
C.eval(ry.FS.accumulatedCollisions, [])
[17]:
(array([0.57247765]),
array([[ 0.5810677 , 1.0999702 , -0.74220228, 1.70678119]]))
Enabling/disabling collisions
In a typical robotic configuration we might have both, visual and (more coarse) collision shapes. Broadphase collision checking should only include the collision shapes. Further, after broadphase collision checking one typically wants to filter out some collisions: shapes of consecutive robot links should usually not be included in collision checking.
In the rai code, shapes have an integer contact
parameter to control this, with the following semantics:
contact = 0 (default) –> the shape never collides (e.g., is a visual)
contact = 1 –> the shape collides with all other shapes
contact < -k (some negative number) –> the shape collides with all shapes except for those that are k-th order parents
So, the latter setting is very natural in robot chains: contact=-1 means “not colliding with the parent link”. To be precise, the “k-th order parenthood” means frames between which there are k joints or less.
Note contact
needs to be set before the first broadphase collision checking, as it is used in the construction of the underlying collision engine.
[18]:
del C
[ ]: