import numpy as np
def quat_mul(p, q):
pw, px, py, pz = p; qw, qx, qy, qz = q
return np.array([pw*qw-px*qx-py*qy-pz*qz,
pw*qx+px*qw+py*qz-pz*qy,
pw*qy-px*qz+py*qw+pz*qx,
pw*qz+px*qy-py*qx+pz*qw])
def axis_angle_to_quat(k_hat, theta):
w = np.cos(theta/2.); v = np.sin(theta/2.)*k_hat
return np.array([w,v[0],v[1],v[2]])
def make_dq(qr, t):
qd = 0.5 * quat_mul(np.array([0.,t[0],t[1],t[2]]), qr)
return qr, qd # each shape (4,)
def dq_mul(p_r, p_d, q_r, q_d):
"""Dual quaternion product (pr + ε pd)(qr + ε qd)."""
r_out = quat_mul(p_r, q_r) # shape (4,)
d_out = quat_mul(p_r, q_d) + quat_mul(p_d, q_r) # shape (4,)
return r_out, d_out
def dq_to_t(qr, qd):
"""Extract translation from dual quaternion."""
qr_conj = np.array([qr[0], -qr[1], -qr[2], -qr[3]]) # shape (4,)
t_pure = 2. * quat_mul(qd, qr_conj) # shape (4,)
return t_pure[1:] # shape (3,)
# Transform 1: 45° about z, translate (1,0,0)
qr1, qd1 = make_dq(axis_angle_to_quat(np.array([0.,0.,1.]), np.pi/4),
np.array([1., 0., 0.]))
# Transform 2: 45° more about z, translate (0,1,0)
qr2, qd2 = make_dq(axis_angle_to_quat(np.array([0.,0.,1.]), np.pi/4),
np.array([0., 1., 0.]))
# Compose: apply T1 first, then T2
qr_c, qd_c = dq_mul(qr2, qd2, qr1, qd1)
print(f"Combined rotation (qr): {qr_c.round(6)}")
print(f"Combined translation: {dq_to_t(qr_c, qd_c).round(6)}")
# Compare with 4×4 matrix composition
def make_T(R, t):
T = np.eye(4); T[:3,:3]=R; T[:3,3]=t; return T
def quat_to_mat(q):
w,x,y,z = q/np.linalg.norm(q)
return np.array([[1-2*(y*y+z*z),2*(x*y-w*z),2*(x*z+w*y)],
[2*(x*y+w*z),1-2*(x*x+z*z),2*(y*z-w*x)],
[2*(x*z-w*y),2*(y*z+w*x),1-2*(x*x+y*y)]])
T1 = make_T(quat_to_mat(qr1), dq_to_t(qr1, qd1))
T2 = make_T(quat_to_mat(qr2), dq_to_t(qr2, qd2))
T_c = T2 @ T1
print(f"\n4×4 translation: {T_c[:3,3].round(6)}")
print(f"Match: {np.allclose(dq_to_t(qr_c, qd_c), T_c[:3,3])}")