Convention#
SAP Warp uses two closely related conventions at the solver boundary. The
public convention is the one scene loaders, benchmark loops, and user code
should normally see. The sap convention is the angular-first,
body-origin convention consumed by the SAP kernels.
Generalized Position Storage#
joint_q is a packed generalized-position array. Joint j stores its
position coordinates starting at joint_q_start[j]; the next entry in
joint_q_start gives the end of the segment. These position coordinates have
fixed storage and do not have a public/sap order flag.
Quaternions are stored in Warp’s xyzw layout:
[x, y, z, w]
The per-joint joint_q segment is:
Joint type |
|
Meaning |
|---|---|---|
|
|
Translation along the joint axis. |
|
|
Rotation angle about the joint axis, in radians. |
|
|
Quaternion rotation. |
|
empty |
No position coordinates. |
|
|
Translation followed by quaternion rotation. |
|
|
Linear-axis coordinates first, then angular-axis coordinates, in the model’s stored axis order. |
For example, free-joint positions store translation followed by the quaternion:
[p_x, p_y, p_z, q_x, q_y, q_z, q_w]
Ball-joint positions store only the quaternion:
[q_x, q_y, q_z, q_w]
The quaternion storage order is independent of velocity ordering. For example,
sap velocity order for a free joint is angular-first, but the free-joint
position still stores translation first and quaternion second. During
integration, the solver maps the solved SAP velocity back through the joint
kinematics before writing state_out.joint_q and state_out.joint_qd.
Consequently, joint_q and joint_qd do not always have the same segment
length. Ball joints store four position scalars but have three velocity DOFs;
free and distance joints store seven position scalars but have six velocity
DOFs.
Generalized Velocity and Force Storage#
The distinction matters only for joints whose generalized velocity represents a
free rigid-body twist: free joints and distance joints. Scalar joints
(revolute and prismatic), ball joints, fixed joints, and general
multi-axis joints keep their stored DOF order. They may still be copied into
temporary fp64 buffers, but they do not need the reference-point transformation
derived below.
Unchanged DOF Segments#
For these joints, the public boundary adapter does not reorder the per-DOF
arrays. Entry k in a joint’s joint_qd segment is copied to entry k
in the SAP buffer, and the matching joint_f, target, limit, and armature
entries keep the same meaning.
Inside the solver, each scalar DOF is then expanded into a spatial-vector column using the joint axis and current pose. That internal spatial vector uses SAP’s angular-first layout, but this expansion does not change the order of the packed per-DOF arrays.
Joint type |
Stored DOF segment |
Boundary treatment |
Why no reference-point shift is needed |
|---|---|---|---|
|
One linear-axis scalar. |
|
The value is a speed or force along one declared axis, not a 6D rigid-body twist. |
|
One angular-axis scalar. |
|
The value is an angular speed or torque about one declared axis, with no linear component to move between COM and body origin. |
|
Three angular scalars. |
The three entries are copied in their stored order. |
The segment contains only angular velocity or torque components. There is no paired linear velocity or force component whose reference point can change. |
|
Empty. |
Nothing is copied. |
The joint has no velocity or force DOFs. |
|
Linear-axis scalars first, then angular-axis scalars, matching
|
Every entry is copied at the same offset; linear entries are not moved after angular entries. |
Each scalar is tied to its declared axis, and the motion subspace maps that scalar into the internal spatial-vector layout. |
The angular-first SAP convention applies after the solver has assembled an actual spatial vector. It does not reorder these packed generalized-coordinate segments at the public boundary.
Changed DOF Segments#
Free and distance joints do change at the public/SAP boundary. Their six velocity or generalized-force entries form a complete rigid-body twist or wrench, so conversion must both reorder the angular and linear blocks and shift the linear velocity or moment between the center of mass and body origin.
Notation#
For one child body, let:
Symbol |
Meaning |
|---|---|
\(W\) |
World frame. All vector components in this page are expressed in \(W\). |
\(O\) |
Child body origin, i.e. the body frame origin stored by runtime body poses. |
\(C\) |
Child body center of mass. |
\(r_{OC}^W\) |
Vector from \(O\) to \(C\), expressed in world coordinates. |
\(v_O^W\), \(v_C^W\) |
Linear velocity of the body origin and center of mass. |
\(\omega^W\) |
Angular velocity. |
\(f^W\) |
Applied force. |
\(\tau_O^W\), \(\tau_C^W\) |
Moment measured about \(O\) or \(C\). |
Define the skew operator \([a]_\times\) by
The only geometric operation in the convention conversion is a reference-point shift. It is not a frame rotation. Because every component is already expressed in \(W\), changing from \(C\) to \(O\) only adds cross-product terms.
Velocity Reference-Point Shift#
Rigid-body linear velocity at two points on the same body satisfies
Equivalently,
The public free-joint velocity is stored with linear COM velocity first:
The SAP free-joint velocity is stored with angular velocity first, and its linear component is measured at the body origin:
Therefore
The inverse conversion used when writing a public state_out is
or, in vector form,
Force and Wrench Duality#
Forces and velocities must transform as dual coordinates: the instantaneous power must not depend on whether the twist is represented at \(C\) or \(O\). The public wrench is
while the SAP generalized force stores moment first, measured about the body origin:
The moment shift is
Thus
This force transform is the inverse transpose of the velocity transform:
That identity gives the power check:
This is the reason SAP Warp shifts both velocity and force. Changing only the array order would produce the right shape but the wrong generalized work.
Free and Distance Joint Arrays#
For free and distance joints, the six velocity entries are:
The corresponding force entries are:
For all other joint types, SAP Warp treats the joint-space arrays as already in the model’s stored order:
External Body Forces#
SapState.body_f uses the same physical public wrench convention as
free-joint generalized forces:
The free-motion kernels assemble an inverse-dynamics residual. In that residual the applied external wrench enters with a negative sign, and the internal body force buffer is angular-first and body-origin:
The negative sign is not a user-facing sign convention. It is an internal
residual convention used by the free-motion solve. Callers should normally keep
SapState.body_f in public order and let
step() build the internal buffer.
Boundary Flags#
The boundary dataclasses store explicit flags so the solver knows whether a buffer is already SAP-native or should be converted:
Pose and generalized-position buffers do not have a public/sap order
flag. Their storage is fixed; only velocity and force-like buffers need the
boundary convention below.
Buffer |
Flag |
|
|
|---|---|---|---|
|
|
Free/distance joints store \([v_C^W,\omega^W]\). |
Free/distance joints store \([\omega^W,v_O^W]\). |
|
|
Free/distance joints store \([f^W,\tau_C^W]\). |
Free/distance joints store \([\tau_O^W,f^W]\). |
|
|
Body wrenches store \([f^W,\tau_C^W]\). |
Body wrenches are already internal angular-first residual forces. |
state() and
control() return public-order buffers. The raw
dataclass defaults are "sap" because internal kernels also construct
temporary dataclass views after conversion.
Solver Boundary Algorithm#
At a timestep boundary, step() follows this
sequence:
If
state_in.joint_qd_order == "public", convert free and distance joint velocities with \(P_v(r)\).If
control.joint_f_order == "public", convert free and distance joint forces with \(P_f(r)\).If
state_in.body_f_order == "public", convert external body forces with \(-P_f(r)\).Run free motion, contact Jacobian assembly, and contact solve in SAP order.
If
state_out.joint_qd_order == "public", convert solved velocities back with \(P_v(r)^{-1}\).
The conversion is repeated each step because \(r_{OC}^W\) depends on the current body orientation:
Practical Rule#
For application code, use state() and
control(), leave joint_qd_order,
joint_f_order, and body_f_order as "public", and let
step() perform the boundary conversion. Use
"sap" only when an array has already been converted into angular-first,
body-origin form and you want to bypass the public boundary adapter.