RelativeTranslationFactor<d> is the translational half of a relative pose measurement, written so that it stays quadratic in every variable it touches. It is the piece that lets certifiable synchronization reuse the same Burer-Monteiro machinery as rotation averaging.
A standard BetweenFactor<Pose3> couples rotation and translation through a nonlinear residual. Splitting the pose into a rotation key and a translation key, and keeping the translation residual in the ambient linear form, gives a cost that the QCQP lowering can express exactly.
import numpy as np
import gtsam
from gtsam.symbol_shorthand import R, T
np.set_printoptions(precision=4, suppress=True)Residual¶
For an edge with measured body-frame offset and translational precision , the factor contributes
evaluateError returns the weighted residual under a unit noise model, so the squared residual reproduces the term above. The factor is quadratic in , , and jointly: enters linearly through , and the translations enter linearly. That is exactly the structure a QCQP needs.
Together with FrobeniusBetweenFactor<Rot3> on the same edge, the pair reproduces the usual synchronization objective
measured = np.array([0.4, -0.2, 0.7])
weight = 2.5
factor = gtsam.RelativeTranslationFactor3(R(0), T(0), T(1), measured, weight)
rotation = gtsam.Rot3.RzRyRx(0.2, -0.1, 0.3)
ti = np.array([1.0, 2.0, 3.0])
tj = np.array([1.5, 1.0, 3.25])
values = gtsam.Values()
values.insert(R(0), rotation)
values.insert(T(0), gtsam.Point3(ti))
values.insert(T(1), gtsam.Point3(tj))
print("residual:", factor.unwhitenedError(values))
print("by hand: ", np.sqrt(weight) * (tj - ti - rotation.matrix() @ measured))residual: [ 0.1303 -1.2308 -0.6847]
by hand: [ 0.1303 -1.2308 -0.6847]
A measurement generated from two consistent poses gives an exactly zero residual, which is the sanity check worth running whenever the body-frame convention is in doubt: .
pose_i = gtsam.Pose3(gtsam.Rot3.RzRyRx(0.1, 0.2, -0.3), np.array([0.5, -1.0, 2.0]))
pose_j = gtsam.Pose3(gtsam.Rot3.RzRyRx(-0.2, 0.4, 0.1), np.array([1.5, 0.5, 1.0]))
consistent = pose_i.rotation().unrotate(pose_j.translation() - pose_i.translation())
exact = gtsam.RelativeTranslationFactor3(R(0), T(0), T(1), consistent, 1.0)
consistent_values = gtsam.Values()
consistent_values.insert(R(0), pose_i.rotation())
consistent_values.insert(T(0), gtsam.Point3(pose_i.translation()))
consistent_values.insert(T(1), gtsam.Point3(pose_j.translation()))
print("residual at the generating poses:", exact.unwhitenedError(consistent_values))residual at the generating poses: [0. 0. 0.]
QCQP lowering¶
qcqpFactors rewrites the term as a single quadratic cost over the stacked row-space variable. With folded in, the row vector
spans the rotation block, pose ’s translation, and pose ’s translation, and the cost matrix is with block dimensions . The rotation contributes rows to the lifted variable and each translation contributes a single row.
The factor emits no constraints: translations are unconstrained in synchronization, and the orthogonality constraints on come from the rotation factors on the same edge.
graph = gtsam.NonlinearFactorGraph()
graph.add(gtsam.RelativeTranslationFactor3(R(0), T(0), T(1), measured, weight))
# Lower to a rank-3 QCQP: one quadratic cost, no equality or inequality constraints.
problem = gtsam.QcqpProblem(graph, 3)
print("costs, equalities, inequalities:", problem.dim())
# Lifted variables: the rotation block is d-by-p, each translation is 1-by-p.
lifted = gtsam.Values()
lifted.insert(R(0), rotation.matrix().T)
lifted.insert(T(0), ti.reshape(1, 3))
lifted.insert(T(1), tj.reshape(1, 3))
print("cost, equality violation, inequality violation:", problem.evaluate(lifted))
residual = factor.unwhitenedError(values)
print("0.5 * squared residual:", 0.5 * residual @ residual)costs, equalities, inequalities: (1, 0, 0)
cost, equality violation, inequality violation: (1.0003141852358166, 0.0, 0.0)
0.5 * squared residual: 1.0003141852358168
API¶
| C++ | Python | Rotation type |
|---|---|---|
RelativeTranslationFactor<2> | RelativeTranslationFactor2 | Rot2 |
RelativeTranslationFactor<3> | RelativeTranslationFactor3 | Rot3 |
The constructor takes (rotationKey, translationKey1, translationKey2, measured, weight), where weight is the translational precision and must be positive.
Practical notes¶
The factor is meant to be paired with a rotation factor on the same edge, keyed so that the rotation key is shared. When converting an anisotropic BetweenFactor<Pose3> covariance, reduce it to two isotropic precisions: from the rotational block and from the translational block. qcqpFactors requires the staircase column dimension to be at least .
For the full certifiable pipeline built on this factor, see CertifiablePoseGraphOptimizationPose2 and CertifiablePoseGraphOptimizationPose3.