I was reading this paper: Closed-Loop Manipulator Control Using Quaternion Feedback, and it mentioned that you could control the end-effector orientation using a resolved rate controller where the orientation part of that controller is: $$\tau_\theta = \mathbf{K}_d(\mathbf{J}_o^\dagger(\omega_d - \mathbf{K}_o\mathbf{e}_o) - \dot{\theta}).$$
See Eq.(20) of the linked paper. My understanding of the terms is that $\omega_d$ is the desired angular velocity, $\mathbf{J}_o$ is the orientation part of the geometric Jacobian, $\dot{\theta}$ is the angular velocities of the joints, $\mathbf{e}_o$ is the orientation error, and both $\mathbf{K}_d$ and $\mathbf{K}_o$ are gain matrices.
In that paper, it mentioned that $\mathbf{e}_o$ could be represented by $\delta{\mathbf{q}}$, which is the vector part of the error quaternion. I did try this. However, it did not work. Instead, if I set $\mathbf{e}_o$ to be the orientation error in the axis-angle form, it was able to give me a controller that works.
So I was wondering if I did something wrong when using the vector part of the error quaternion as $\delta{\mathbf{q}}$. I guess that one possible place I might have gotten wrong is $\mathbf{J}_o$, I am using the rotation part of the geometric Jacobian, is this the right choice?
The portion of my code that computes the control is
import pinocchio as pin
from scipy.spatial.transform import Rotation as R
# get joint states
q, dq = get_state()
# Get end-effector position
ee_position = get_ee_position()
# Get end-effector orientation in quaternions
ee_orientation = get_ee_orientation()
# [x, y, z, w]
ee_quaternion = R.from_matrix(ee_orientation).as_quat()
# Orientation error in quaternion form
quat_err = compute_quat_vec_error(target_quaternion, ee_quaternion)
# Get frame ID for grasp target
jacobian_frame = pin.ReferenceFrame.LOCAL_WORLD_ALIGNED
# Get Jacobian of grasp target frame
jacobian = robot.getFrameJacobian(FRAME_ID, jacobian_frame)
# Get pseudo-inverse of frame Jacobian
pinv_jac = np.linalg.pinv(jacobian)
# Compute Gravitational terms
G = robot.gravity(q)
# Compute controller
target_dx = np.zeros((6, 1))
target_dx[:3] = 1.0 * (target_position - ee_position)
target_dx[3:] = np.diag([3.0, 3.0, 3.0]) @ quat_err[:, np.newaxis]
# Compute torque commands
tau = (pinv_jac @ target_dx - dq[:, np.newaxis]) + G[:, np.newaxis]
the function that I used to compute the quaternion error is
def compute_quat_vec_error(quat_desired, quat_measured):
eta_d = quat_desired[-1]
eta = quat_measured[-1]
q_d = quat_desired[:3]
q = quat_measured[:3]
delta_quat_vec = eta_d * q - eta * q_d - np.cross(q_d, q)
return delta_quat_vec