I'm trying to find contact Jacobian defined as M(q) q_ddot + C(q, q_dot) = G(q) + B(q) u + J'(q) lambda.
I tried to mirror the function CalcNormalAndTangentContactJacobians()
as in multibody_plant.cc
link but the values are not adding up.
Here is how I setup the system (for the simplest setup possible):
I have a cylinder mounted to world through a revolute joint and a ball that can freely move in the x-y plant through dummy prismatic joints. I run the simulation that drives the joint at constant torque to make contact with the ball and push it off. I log the states of the system through simulation and find an instance when there is contact and look at that particular instance.
I set the MultibodyPlant(MBP) to the states as exactly in the simulation by SetPositionsAndVelocities
and SetActuationInArray
, and get the relative variables M, Cv, tauG, B, q_ddot, tauExt, and contactResults.
Now I'm ready to find the contact Jacobian. (Updated in reflection to the two suggestions from @Alejandro)
# CalcNormalAndTangentContactJacobians
Jn = np.zeros((num_contact, plant.num_velocities()))
Jt = np.zeros((2*num_contact, plant.num_velocities()))
if num_contact > 0:
frame_W = plant.world_frame()
for i in range(num_contact):
penetration_point_pairs = query_object.ComputePointPairPenetration()
penetration_point_pair = penetration_point_pairs[0]
geometryA_id = penetration_point_pair.id_A;
geometryB_id = penetration_point_pair.id_B;
linkA = get_name_by_geometryId(plant, geometryA_id)
bodyA = plant.GetBodyByName(linkA)
linkB = get_name_by_geometryId(plant, geometryB_id)
bodyB = plant.GetBodyByName(linkB)
nhat_BA_W = penetration_point_pair.nhat_BA_W;
p_WCa = penetration_point_pair.p_WCa;
p_WCb = penetration_point_pair.p_WCb;
p_ACa = plant.EvalBodyPoseInWorld(plant_context, bodyA).inverse().multiply(p_WCa)
p_BCb = plant.EvalBodyPoseInWorld(plant_context, bodyB).inverse().multiply(p_WCb)
wrt = JacobianWrtVariable.kV
Jv_WAc = plant.CalcJacobianTranslationalVelocity(plant_context, wrt, \
bodyA.body_frame(), p_ACa, frame_W, frame_W);
Jv_WBc = plant.CalcJacobianTranslationalVelocity(plant_context, wrt, \
bodyB.body_frame(), p_BCb, frame_W, frame_W);
Jn[i,:] = nhat_BA_W.reshape((1,-1)).dot(Jv_WAc - Jv_WBc);
R_WC = ComputeBasisFromAxis(2, nhat_BA_W)
that1_W = R_WC[:,0]
that2_W = R_WC[:,1]
Jt[0 ,:] = that1_W.transpose().dot(Jv_WBc - Jv_WAc)
Jt[1,:] = that2_W.transpose().dot(Jv_WBc - Jv_WAc)
ft = R_WC[:,0:2].T.dot(contact_force)
fn = -R_WC[:,2].dot(contact_force)
## Equivalent to:
#contact_terms = (Jv_WBc - Jv_WAc).T.dot(contact_force)
contact_terms = Jn.T.dot(fn) + Jt.T.dot(ft).flatten()
print("===========================")
print("without contact terms:")
print(M.dot(v_dot) + Cv - tauG - tauExt - B.dot(u) )
print("expected contact forces: ")
print(plant.get_generalized_contact_forces_output_port(model).Eval(plant_context))
print("contact terms:")
print(contact_terms.flatten())
print("residule")
print(M.dot(v_dot) + Cv - tauG - tauExt - B.dot(u) - contact_terms.flatten())
print("l2 norm of residual:")
print(np.linalg.norm(M.dot(v_dot) + Cv - tauG - tauExt - B.dot(u) - contact_terms.flatten()))
print("Jn:")
print(Jn)
print("Jt:")
print(Jt)
Looking at the results through printouts I get:
expected contact forces:
[ 1.12529061 5.90160213 -10.10437394]
contact terms I got:
[ 1.12532645 5.90160213 -10.10437394]
residule
[-3.58441669e-05 -8.88178420e-16 1.77635684e-15]
l2 norm of residual:
3.5844166926812804e-05
Jn:
[[ 0.09613277 0.5110166 -0.85957043]]
Jt:
[[-4.99995256e-03 8.59570734e-01 5.11016784e-01]
[ 8.09411844e-05 4.30262131e-04 -7.23735008e-04]]
Here is the correct contact Jacobian for reference(I get these by deriving the contact Jacobian via definition):
Expected Jn:
[[ 0.09613277 0.5110166 -0.85957043]]
Expected Jt:
[[-4.60458428e-03 8.59570734e-01 5.11016784e-01]
[ 8.09411844e-05 4.30262131e-04 -7.23735008e-04]]
residuals using these:
[ 4.44089210e-16 -8.88178420e-16 1.77635684e-15]
The difference is quite small (for this very simple setup) so maybe it's not completely wrong but the scale of residual is still concerning. Notice that the first term is wrong. In a more complex system, all revolute joints have residules from experimental observation.
I really don't understand what's causing this residual, and this residual is much much larger in a more complex system to a scale equivalent to residuals without contact terms(even when there is just one more joint, I can add more plots if anyone's interested).
For reference, here's the residuals over the coarse of the entire 0.5 sec simulation:
Other things I have tried that also didn't work out:
Using
SignedDistancePair
instead ofPenetrationAsPointPair
byquery_object.ComputeSignedDistancePairClosestPoints(geometryA_id, geometryB_id)
Using relative frames
p_GbCb = signed_distance_pair.p_BCb
p_GaCa = signed_distance_pair.p_ACa
Jv_v_BCa_W = plant.CalcJacobianTranslationalVelocity(plant_context, wrt, frameB, p_GbCb, frameA, frame_W);
Using
JacobianWrtVariable.kQDot
instead ofJacobianWrtVariable.kV
Using
inspector.GetPoseInFrame
to make sure the pose is in the frame we think it should be in as referencing this file
from last point:
p_GaCa = signed_distance_pair.p_ACa
p_ACa = inspector.GetPoseInFrame(signed_distance_pair.id_A).rotation().matrix().dot(p_GaCa)
p_GbCb = signed_distance_pair.p_BCb
p_BCb = inspector.GetPoseInFrame(signed_distance_pair.id_B).rotation().matrix().dot(p_GbCb)
Jv_v_BCa_W = plant.CalcJacobianTranslationalVelocity(plant_context, wrt, frameA, p_ACa, frameB, frame_W);
Solution:
Thanks to @Alejandro, I finally figured out the issue:
contact point is the middle point between p_WCa
and p_WCb
. Using p_WCa
and p_WCb
in CalcJacobianTranslationalVelocity
will result in a small residual as described above. This is perhaps due to how the physics engine is built. While I don't have further insights in explaining how numerical values are calculated under the hood, here I would like to share two ways of finding the contact Jacobian that produce identical results that concludes zero residual in the contact dynamics equation:
Method 1, with PointPairPenetration
penetration_point_pairs = query_object.ComputePointPairPenetration()
penetration_point_pair = penetration_point_pairs[0]
geometryA_id = penetration_point_pair.id_A;
geometryB_id = penetration_point_pair.id_B;
linkA = get_name_by_geometryId(plant, geometryA_id)
bodyA = plant.GetBodyByName(linkA)
linkB = get_name_by_geometryId(plant, geometryB_id)
bodyB = plant.GetBodyByName(linkB)
nhat_BA_W = penetration_point_pair.nhat_BA_W;
p_WCa = penetration_point_pair.p_WCa;
p_WCb = penetration_point_pair.p_WCb;
X_WA = plant.EvalBodyPoseInWorld(plant_context, bodyA)
X_WB = plant.EvalBodyPoseInWorld(plant_context, bodyB)
p_ACa = X_WA.inverse().multiply((p_WCa+p_WCb)/2)
p_BCb = X_WB.inverse().multiply((p_WCa+p_WCb)/2)
wrt = JacobianWrtVariable.kV
Jv_WAc = plant.CalcJacobianTranslationalVelocity(plant_context, wrt, \
bodyA.body_frame(), p_ACa, frame_W, frame_W);
Jv_WBc = plant.CalcJacobianTranslationalVelocity(plant_context, wrt, \
bodyB.body_frame(), p_BCb, frame_W, frame_W);
Jv = Jv_WBc - Jv_WAc
Method 2: with SignedDistancePairClosestPoints
signed_distance_pair = query_object.ComputeSignedDistancePairClosestPoints(geometryA_id, geometryB_id)
inspector = query_object.inspector()
linkA = get_name_by_geometryId(plant, geometryA_id)
bodyA = plant.GetBodyByName(linkA)
linkB = get_name_by_geometryId(plant, geometryB_id)
bodyB = plant.GetBodyByName(linkB)
nhat_BA_W = signed_distance_pair.nhat_BA_W;
p_GaCa = signed_distance_pair.p_ACa;
p_GbCb = signed_distance_pair.p_BCb;
X_AGa = inspector.GetPoseInFrame(signed_distance_pair.id_A) #Reports the pose of the geometry G with the given id in its registered frame F X_FG.
X_BGb = inspector.GetPoseInFrame(signed_distance_pair.id_B)
p_ACa = X_AGa.multiply(p_GaCa)
p_BCb = X_BGb.multiply(p_GbCb)
X_WA = plant.EvalBodyPoseInWorld(plant_context, bodyA)
X_WB = plant.EvalBodyPoseInWorld(plant_context, bodyB)
p_WCa = X_WA.multiply(p_ACa)
p_WCb = X_WB.multiply(p_BCb)
p_ACa = X_WA.inverse().multiply((p_WCa+p_WCb)/2)
p_BCb = X_WB.inverse().multiply((p_WCa+p_WCb)/2)
Jv_WAc = plant.CalcJacobianTranslationalVelocity(plant_context, wrt, \
Residuals
Finally here's how the residuals look with aforementioned method1 and method2. 1e-15 is essentially zero :)