A hand target specifies where the hand should go. The solver also needs to know which robot accelerations move it there. A pose task connects the two: it computes an error, applies feedback, and returns a Jacobian and acceleration target.

WBC 02 prepared the current robot state and model data. This post builds one acceleration-level pose task on that interface.

Choose the coordinates before applying gains

A pose contains position and orientation, but their stored values cannot be compared in the same way. Position error is a subtraction. Orientation error must account for how rotations compose: yaw angles of 179° and −179° are only 2° apart. We therefore compare orientations through the relative rotation RTRdR^TR_d. The rotation logarithm converts it into a three-component axis–angle error that feedback gains can act on. SE(3) extends the relative-transform calculation to position and orientation together.

Let T=(R,p)T=(R,p) be the current hand pose and Td=(Rd,pd)T_d=(R_d,p_d) its reference. Both transforms map hand-frame coordinates to world coordinates. A relative transform describes the target as seen from the current hand:

T−1Td=[RTRdRT(pd−p)01].T^{-1}T_d= \begin{bmatrix} R^TR_d & R^T(p_d-p) \\ 0 & 1 \end{bmatrix}.

A reference can arrive as seven stored values: position and a unit quaternion. The task builds an SE(3) transform from them, compares it with the current pose, and extracts an error for feedback. The stored vector and the transform represent the same pose; the six-component error is a different quantity.

INSIDE A POSE TASK

Where do the seven pose values go?

  1. Pose vector7 values
  2. SE(3) pose4 × 4
  3. Relative pose4 × 4
  4. Local error6 values
  5. World error6 values
  6. Acceleration6 values
1 / 5 · 7 values → pose
Stored pose referenceSE(3) pose T_dCurrent position−=0.100-0.0500.200Orientation: log₃(R_Hᵀ R_d)Yaw-only example: 75° − 30° = 45°0.3000.2500.1500.4000.4000.2000.2000.3500.3500.2590.259-0.966-0.9660.0000.0000.9660.9660.2590.2590.0000.0000.0000.0000.0000.0001.0001.0000.0000.0000.6090.7930001Unit quaternion · xyzwqx = 0, qy = 0qz = 0.609, qw = 0.793R₁₁ = 1 − 2(qy² + qz²)1 − 2 × 0.609²Compute one matrix entry0.2590.0000.0000.0000.0000.7850.785quaternion → R(q)KpP termD = Kd e_va_refx×40→12.000+8 × -0.050-0.400+0=11.600y×40→10.000+8 × -0.020-0.160+0=9.840z×40→6.000+8 × 0.0000.000+0=6.000ωx×10→0.000+4 × 0.0000.000+0=0.000ωy×10→0.000+4 × 0.0000.000+0=0.000ωz×10→7.854+4 × -0.100-0.400+0=7.454Stationary reference: Vd = 0, a_ref = 0P: x acceleration = 40 × 0.300 = 12.000 m/s²Blue: position / linear quantitiesPink: rotation / angular quantities
(pd, quaterniond)  ⟶  Td=[Rdpd01]∈SE(3)(p_d,\,\mathrm{quaternion}_d)\;\longrightarrow\;T_d=\begin{bmatrix}R_d&p_d\\0&1\end{bmatrix}\in SE(3)

Unit quaternion → rotation matrix

R(q)=[1−2(qy2+qz2)2(qxqy−qzqw)2(qxqz+qyqw)2(qxqy+qzqw)1−2(qx2+qz2)2(qyqz−qxqw)2(qxqz−qyqw)2(qyqz+qxqw)1−2(qx2+qy2)],∥q∥=1R(q)=\begin{bmatrix}1-2(q_y^2+q_z^2)&2(q_xq_y-q_zq_w)&2(q_xq_z+q_yq_w)\\2(q_xq_y+q_zq_w)&1-2(q_x^2+q_z^2)&2(q_yq_z-q_xq_w)\\2(q_xq_z-q_yq_w)&2(q_yq_z+q_xq_w)&1-2(q_x^2+q_y^2)\end{bmatrix},\quad\lVert q\rVert=1

The reference stores position and a unit quaternion. Position moves into the last column. With qx = qy = 0, substitute qz and qw into the four nontrivial rotation entries; the other entries are 0 or 1. Displayed coefficients are rounded, but calculations use full precision. The 4×4 transform represents the same desired pose in SE(3); it is not a 7D error vector.

Fixed illustrative 3D poses with yaw-only rotations. For PD feedback, current world-aligned task velocity is [0.05, 0.02, 0, 0, 0, 0.10] (m/s then rad/s); desired velocity and reference acceleration are zero. Target x = 0.40 m, yaw = 75°. Current p = [0.10, −0.05, 0.20] m, yaw = 30°. Target y = 0.20 m, z = 0.35 m. Quaternion order: xyzw. Only the calculation stages animate; no robot dynamics or solver runs here.

Rotation error needs an axis and an angle. The map log⁡3\operatorname{log}_3 returns that three-component rotation vector from RTRdR^TR_d. For this task, express both position and angular error along world-aligned axes:

e=[pd−pR log⁡3(RTRd)],ev=Vd−V.e= \begin{bmatrix} p_d-p \\ R\,\operatorname{log}_3(R^TR_d) \end{bmatrix}, \qquad e_v=V_d-V.

The first three entries have units of metres; the last three have units of radians. VV and VdV_d contain linear then angular velocity, expressed along world axes at their respective frame origins. This is a split position/orientation error. The angular feedback is a local tracking construction, with the usual rotation-log ambiguity at 180°.

Every quantity in the task must follow the same convention. Here the Jacobian and current velocity use Pinocchio’s LOCAL_WORLD_ALIGNED: axes aligned to the world, with the current frame origin as the velocity reference point. This differs from WORLD, whose spatial velocity representation uses the world origin. Pinocchio’s frame API exposes those frame choices.

Why this is not simply log6 of the relative pose

The split error uses relative translation directly and log3 for orientation. The full SE(3) logarithm also couples its translational component to rotation. Replacing one with the other changes the error definition. This example keeps position error interpretable in metres and rotates the angular error into world-aligned axes. For rotation tasks, eve_v denotes a velocity difference, not the exact time derivative of the rotation-vector error.

Turn error into a desired acceleration

For each selected task direction, apply a feedback rule:

a∗=aref+Kpe+Kdev.a^*=a_{\mathrm{ref}}+K_pe+K_de_v.

The reference supplies pose, velocity, and acceleration. For a stationary target, its velocity and acceleration are zero. The gains set how the task requests acceleration in response to error. They do not decide which task wins when two requests conflict.

The model relates robot acceleration to task acceleration:

atask=Jv˙+d,b=a∗−d.a_{\mathrm{task}}=J\dot v+d, \qquad b=a^*-d.

Here dd is the task acceleration when generalized acceleration v˙\dot v is zero. For this world-aligned geometric task, it is the drift term often written J˙v\dot Jv. Use classical linear acceleration for the position rows, paired with angular acceleration for the orientation rows.

The task can now return J,bJ,b, defining a residual Jv˙−bJ\dot v-b. The formulation decides how to use it:

RoleSolver expression
Hard taskJv˙=bJ\dot v=b
Soft taskInclude ∥Jv˙−b∥2\lVert J\dot v-b\rVert^2 in the objective

A task with position-only control selects the first three rows of both JJ and bb. A torso orientation task selects the last three. Masking only one side would change the meaning or size of the request.

Keep the task independent of the model cache

The task owns its reference, gains, and axis selection. It receives model results without updating Pinocchio’s shared data. The following Python example assumes six-component references in the convention above; multiplication by the gain arrays is elementwise.

from dataclasses import dataclass
import numpy as np
import pinocchio as pin

@dataclass
class PoseReference:
    pose: pin.SE3
    velocity: np.ndarray       # [linear, angular], world-aligned
    acceleration: np.ndarray   # [linear, angular], world-aligned

@dataclass
class TaskRequest:
    J: np.ndarray
    b: np.ndarray

class PoseTask:
    def __init__(self, reference, kp, kd, axes):
        self.reference = reference
        self.kp = np.asarray(kp).copy()  # six gains
        self.kd = np.asarray(kd).copy()
        self.axes = np.asarray(axes, dtype=bool)  # six flags

    def evaluate(self, pose, velocity, J, drift):
        ref = self.reference
        R = pose.rotation
        error = np.concatenate([
            ref.pose.translation - pose.translation,
            R @ pin.log3(R.T @ ref.pose.rotation),
        ])
        velocity_error = ref.velocity - velocity
        a_star = (ref.acceleration + self.kp * error
                  + self.kd * velocity_error)
        return TaskRequest(
            J=J[self.axes, :].copy(),
            b=(a_star - drift)[self.axes].copy(),
        )

This example expects pose, velocity, JJ, and drift from one model update. Gains and masks have six entries; model input validation belongs at the application boundary. The TSID pose task uses the same separation between feedback, drift compensation, and selected task rows.

Prepare drift once, then evaluate the task

Extend the RobotModel.update from 02 to prepare acceleration data with v˙=0\dot v=0. The code below belongs inside the model owner, before any task reads its results:

# Inside RobotModel.update(q, v), after computeAllTerms:
pin.forwardKinematics(
    self.model, self._data, q, v, np.zeros(self.model.nv),
)
pin.updateFramePlacements(self.model, self._data)

The model can then provide a hand-motion snapshot in the chosen convention:

# Inside RobotModel.hand_motion(), after update:
rf = pin.LOCAL_WORLD_ALIGNED
pose = self._data.oMf[self._hand_id].copy()
velocity = pin.getFrameVelocity(
    self.model, self._data, self._hand_id, rf,
).vector.copy()
J = pin.getFrameJacobian(
    self.model, self._data, self._hand_id, rf,
).copy()
drift = pin.getFrameClassicalAcceleration(
    self.model, self._data, self._hand_id, rf,
).vector.copy()
return pose, velocity, J, drift

getFrameClassicalAcceleration reads the acceleration data prepared above. Because generalized acceleration was set to zero, this result is drift rather than the full commanded acceleration. The frame acceleration API describes this use for acceleration-level constraints.

One controller update now has a short task boundary:

robot.update(q, v)
hand_request = hand_task.evaluate(*robot.hand_motion())
# Other tasks use the same model update.
# The formulation assembles their J, b requests with dynamics and contacts.

The model owner provides geometry and dynamics; the task provides feedback; the formulation assigns constraints, weights, and priority. WBC 04 takes these requests and examines how to combine them when they conflict.