Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Galilean IMU preintegration with a left-invariant error

This guide develops the left-invariant Galilean IMU preintegration used by GTSAM: a direct-product symmetry, compatible left actions, exact zero-order-held Galilean increments, adjoint covariance transport, right-applied bias correction, and exact constant-rate rotating-frame prediction.

The construction is motivated by Delama, Fornasier, Mahony, and Weiss, Equivariant IMU Preintegration with Biases: a Galilean Group Approach, but deliberately uses a different symmetry and perturbation convention. It closes on the physical six-dimensional IMU input and bias in accelerometer-then-gyroscope order, and it induces the same left-invariant, component-wise right-retracted endpoint error used by GTSAM.

For background, see the EquivariantFilter guide, the Gal3 guide, the Gal3ImuEKF guide, the NavState guide, and the standard ImuFactor guide. The companion NEES comparison evaluates all four GTSAM preintegration backends under identical high-dynamic IMU samples.

Open In Colab
import numpy as np
import gtsam

1. Conventions: left-invariant error means an update on the right

We fix the convention before introducing the dynamics. For a matrix Lie group GG with Lie algebra g\mathfrak g, GTSAM uses

OperationDefinitionConsequence
RetractionXδ=XExp(δ)X\oplus\delta = X\operatorname{Exp}(\delta)A tangent increment is applied on the right.
Local coordinatesLocal(X,Y)=Log(X1Y)\operatorname{Local}(X,Y)=\operatorname{Log}(X^{-1}Y)The displacement is expressed in the local frame of XX.
Error about X^\hat XX=X^Exp(ϵ)X=\hat X\operatorname{Exp}(\epsilon)ϵ=Local(X^,X)\epsilon=\operatorname{Local}(\hat X,X).

The group error X^1X\hat X^{-1}X is left-invariant: replacing both arguments by AXAX and AX^A\hat X leaves it unchanged. Thus, throughout this guide, left-invariant error and right perturbation describe the same convention.

Delama et al.'s motivating construction instead uses the navigation error ΥΥ^1\Upsilon\hat\Upsilon^{-1}, which is right-invariant, together with a left-applied first-order correction. Reversing only the error while retaining their right action would not produce an invariant EqF error; the direct-product left action introduced in Section 5 is what induces Υ^1Υ\hat\Upsilon^{-1}\Upsilon.

Do not conflate multiplication sides. ActionType::Left below describes how the symmetry moves the physical state and input. GTSAM’s EquivariantFilter prediction still composes the lifted increment on the right, while its measurement update composes the innovation on the left. The side of a symmetry action, the side of a group increment, and the invariance of an error are related choices, but they are not synonyms.

2. Physical state

The preintegration state consists of the accumulated Galilean motion and the physical six-axis IMU bias:

M=Gal(3)×R6,ξk=(Υk,βk),βk=[ba,kbω,k].\mathcal M=\mathrm{Gal}(3)\times\mathbb R^6,\qquad \xi_k=(\Upsilon_k,\beta_k),\qquad \beta_k=\begin{bmatrix}b_{a,k}\\b_{\omega,k}\end{bmatrix}.

Here Υk=(ΔRk,Δvk,Δpk,Δtk)\Upsilon_k=(\Delta R_k,\Delta v_k,\Delta p_k,\Delta t_k) is the motion being preintegrated. The second component is exactly gtsam::imuBias::ConstantBias; its vector representation stores accelerometer bias before gyroscope bias. There are no additional bias coordinates. Bias perturbations and bias-Jacobian columns retain this ordering throughout.

3. The Galilean motion component

A Galilean element stores rotation, velocity, position, and elapsed time:

X=(R,v,p,t)X=[Rvp01t001].X=(R,v,p,t) \quad\longleftrightarrow\quad \mathbf X= \begin{bmatrix} R & v & p\\ 0 & 1 & t\\ 0 & 0 & 1 \end{bmatrix}.

Matrix multiplication gives

(R1,v1,p1,t1)(R2,v2,p2,t2)=(R1R2,  v1+R1v2,  p1+R1p2+t2v1,  t1+t2).(R_1,v_1,p_1,t_1)(R_2,v_2,p_2,t_2) =\left(R_1R_2,\;v_1+R_1v_2,\;p_1+R_1p_2+t_2v_1,\;t_1+t_2\right).

The term t2v1t_2v_1 is the defining Galilean coupling: during the second interval, the velocity accumulated by the first interval advances position. The inverse is

(R,v,p,t)1=(R,Rv,R(ptv),t).(R,v,p,t)^{-1}=\left(R^\top,-R^\top v,-R^\top(p-tv),-t\right).

Using GTSAM’s tangent ordering, an algebra element is

x=(ω,ν,ρ,α)R10,x=[ωνρ00α000].x=(\omega,\nu,\rho,\alpha)\in\mathbb R^{10}, \qquad x^\wedge= \begin{bmatrix} \omega^\wedge & \nu & \rho\\ 0 & 0 & \alpha\\ 0 & 0 & 0 \end{bmatrix}.

For an integrated IMU increment, ω\omega is an angle, ν\nu a velocity increment, ρ\rho a position increment, and α\alpha an elapsed time. Before multiplication by Δt\Delta t, the corresponding rate vector has units (rad/s,m/s2,m/s,1)({\rm rad/s}, {\rm m/s^2}, {\rm m/s}, 1).

Exponential, Jacobians, and adjoint

Let JL(ω)J_L(\omega) be the SO(3)SO(3) left Jacobian and Γ2(ω)\Gamma_2(\omega) its second integral. Then

Exp(x)=(Exp(ω),JL(ω)ν,JL(ω)ρ+αΓ2(ω)ν,α).\operatorname{Exp}(x)= \left( \operatorname{Exp}(\omega), J_L(\omega)\nu, J_L(\omega)\rho+\alpha\Gamma_2(\omega)\nu, \alpha \right).

The αΓ2(ω)ν\alpha\Gamma_2(\omega)\nu term produces the familiar 12aΔt2\tfrac12a\Delta t^2 when rotation is zero. For the complete 10D algebra, define the right Jacobian JR(x)J_R(x) by

Exp(x+δx)=Exp(x)Exp(JR(x)δx)+O(δx2).\operatorname{Exp}(x+\delta x) =\operatorname{Exp}(x) \operatorname{Exp}(J_R(x)\delta x)+O(\|\delta x\|^2).

This is the Jacobian required when both the state perturbation and the increment are applied on the right. It is related to the complete Galilean left Jacobian by

JR(x)=AdExp(x)JL(x).J_R(x)=\operatorname{Ad}_{\operatorname{Exp}(-x)}J_L(x).

For X=(R,v,p,t)X=(R,v,p,t), the adjoint in the ordering (ω,ν,ρ,α)(\omega,\nu,\rho,\alpha) is

AdX=[R000vRR00(ptv)RtRRv0001].\operatorname{Ad}_X= \begin{bmatrix} R & 0 & 0 & 0\\ v^\wedge R & R & 0 & 0\\ (p-tv)^\wedge R & -tR & R & v\\ 0 & 0 & 0 & 1 \end{bmatrix}.

We write adxy=[x,y]\operatorname{ad}_x y=[x,y] for the algebra adjoint and use the identities AdX1=AdX1\operatorname{Ad}_{X^{-1}}=\operatorname{Ad}_X^{-1} and adxy=adyx\operatorname{ad}_x y=-\operatorname{ad}_y x.

4. Mean preintegration

The measured input is the physical six-axis IMU sample supplied to GTSAM’s integrateMeasurement(measuredAcc, measuredOmega, deltaT) interface:

uk=[akωk]L=R6.u_k=\begin{bmatrix}a_k\\\omega_k\end{bmatrix} \in\mathcal L=\mathbb R^6.

It follows the same accelerometer-then-gyroscope ordering as the bias. Holding this sample constant over an interval of duration hh, its bias-corrected Galilean body rate is

q(uk,βk)=[ωkbω,kakba,k031].q(u_k,\beta_k)=\begin{bmatrix} \omega_k-b_{\omega,k}\\ a_k-b_{a,k}\\ 0_3\\ 1 \end{bmatrix}.

The exact discrete dynamics for this held input are

Fh((Υk,βk),uk)=(ΥkExp(q(uk,βk)h),  βk).F_h((\Upsilon_k,\beta_k),u_k)= \left(\Upsilon_k\operatorname{Exp}(q(u_k,\beta_k)h),\;\beta_k\right).

The bias is constant inside preintegration because the three-way IMU factor uses a separate factor for bias evolution. No bias-process input is part of L\mathcal L. For a measured sample u~k=(a~k,ω~k)\tilde u_k=(\tilde a_k,\tilde\omega_k) and linearization bias β^k\hat\beta_k, let

qk=q(u~k,β^k),xk=qkh,Vk=Exp(xk).q_k=q(\tilde u_k,\hat\beta_k),\qquad x_k=q_kh,\qquad V_k=\operatorname{Exp}(x_k).

Starting with Υ^ii=I\hat\Upsilon_{ii}=I, the preintegrated mean is updated on the right:

Υ^i,k+1=Υ^ikVk.\boxed{\hat\Upsilon_{i,k+1}=\hat\Upsilon_{ik}V_k}.

The group law automatically performs all four component updates. In particular, old velocity advances position by hΔvikh\,\Delta v_{ik}, while the exponential contributes the rotation-aware acceleration integral. No separate Euler position update is needed.

Gal(3) is internal to preintegration; the optimized keyframe states remain Xi,XjSE2(3)X_i,X_j\in\mathrm{SE}_2(3). NavState stores and retracts it as (R,p,v)(R,p,v), and we define

Wij=NavState ⁣(I,12gΔtij2,gΔtij),ϕΔt(R,p,v)=(R,p+vΔt,v),W_{ij}=\operatorname{NavState}\!\left(I,\tfrac12g\Delta t_{ij}^2,g\Delta t_{ij}\right),\qquad \phi_{\Delta t}(R,p,v)=(R,p+v\Delta t,v),

and project the corrected Υij\Upsilon_{ij} to the body-frame increment

Uij(βi)=NavState(ΔRij,Δpij,Δvij).U_{ij}(\beta_i)=\operatorname{NavState}(\Delta R_{ij},\Delta p_{ij},\Delta v_{ij}).

The inertial endpoint prediction is exactly

Xj=WijϕΔtij(Xi)Uij(βi).\boxed{X_j=W_{ij}\,\phi_{\Delta t_{ij}}(X_i)\,U_{ij}(\beta_i)}.

This Wϕ(X)UW\phi(X)U separation is important: gravity acts in the navigation frame on the left, autonomous coasting advances the existing position, and bias-corrected IMU motion acts in the body/local frame on the right.

5. The equivariant-filter construction

GTSAM’s EquivariantFilter<M, Symmetry> separates the physical state manifold MM from a symmetry group GG that moves points of MM. A complete model also supplies an action on the input space and a lift that reconstructs the dynamics on GG. For this factor the template roles are

EqF roleGalilean preintegration object
Physical stateξ=(Υ,β)M\xi=(\Upsilon,\beta)\in\mathcal M
Symmetry groupY=(Γ,c)GLY=(\Gamma,c)\in\mathcal G_L
State actionϕL:GL×MM\phi_L:\mathcal G_L\times\mathcal M\to\mathcal M
Input actionψL:GL×LL\psi_L:\mathcal G_L\times\mathcal L\to\mathcal L
LiftλL:M×LLie(GL)\lambda_L:\mathcal M\times\mathcal L\to\operatorname{Lie}(\mathcal G_L)

Direct-product symmetry group

Choose the direct product

GL=Gal(3)×(R6,+),\mathcal G_L=\mathrm{Gal}(3)\times(\mathbb R^6,+),

where the second factor is the additive physical bias space in GTSAM order. For Y1=(Γ1,c1)Y_1=(\Gamma_1,c_1) and Y2=(Γ2,c2)Y_2=(\Gamma_2,c_2),

Y1Y2=(Γ1Γ2,c1+c2),Y1=(Γ1,c).Y_1Y_2=(\Gamma_1\Gamma_2,c_1+c_2),\qquad Y^{-1}=(\Gamma^{-1},-c).

This group has the same dimension as M\mathcal M, but its role is different: M\mathcal M contains physical states, whereas GL\mathcal G_L supplies transformations of those states.

Left action on the state

Define

ϕL((Γ,c),(Υ,β))=(ΓΥ,β+c).\boxed{\phi_L((\Gamma,c),(\Upsilon,\beta)) =(\Gamma\Upsilon,\beta+c)}.

It is a left action because

ϕL(Y1,ϕL(Y2,ξ))=ϕL(Y1Y2,ξ).\phi_L(Y_1,\phi_L(Y_2,\xi)) =\phi_L(Y_1Y_2,\xi).

In GTSAM terminology the corresponding Symmetry derives from GroupAction and declares ActionType::Left; its call signature is operator()(group, state). Fix the reference state

ξ=(I,0).\xi^\circ=(I,0).

The orbit map used by EquivariantFilter is especially simple:

ϕξ(Y)=ϕL(Y,ξ)=(Γ,c).\phi_{\xi^\circ}(Y)=\phi_L(Y,\xi^\circ)=(\Gamma,c).

Consequently the lifted group estimate and the physical estimate have identical coordinates,

Y^=(Υ^,β^),ξ^=ϕξ(Y^).\hat Y=(\hat\Upsilon,\hat\beta),\qquad \hat\xi=\phi_{\xi^\circ}(\hat Y).

Left action on the input

The compatible input action is

ψL((Γ,c),u)=u+c.\boxed{\psi_L((\Gamma,c),u)=u+c}.

It is also a left action. The Galilean component Γ\Gamma acts trivially on the physical IMU input, while c=(ca,cω)c=(c_a,c_\omega) translates it in the same GTSAM order as the bias. Consequently

q(u+c,β+c)=q(u,β),q(u+c,\beta+c)=q(u,\beta),

and direct substitution verifies system equivariance:

Fh(ϕL(Y,ξ),ψL(Y,u))=ϕL(Y,Fh(ξ,u)).F_h(\phi_L(Y,\xi),\psi_L(Y,u)) =\phi_L(Y,F_h(\xi,u)).

The EquivariantFilter maps an input to the reference orbit by applying its InputOrbit to the inverse group estimate. Here

u=ψL(Y^1,u~)=u~β^.u^\circ=\psi_L(\hat Y^{-1},\tilde u) =\tilde u-\hat\beta.

Thus the input at the EqF origin is exactly the bias-corrected body/local input; no adjoint transport is required.

Lift and reconstruction

Use the right-trivialized lift expected by GTSAM’s prediction path,

λL((Υ,β),u)=(q(u,β),06)gal(3)×R6.\boxed{\lambda_L((\Upsilon,\beta),u) =\left(q(u,\beta),0_6\right) \in\mathfrak{gal}(3)\times\mathbb R^6}.

The direct-product exponential gives

ExpGL(λL(ξ,u)h)=(Exp(q(u,β)h),06).\operatorname{Exp}_{\mathcal G_L}(\lambda_L(\xi,u)h) =\left(\operatorname{Exp}(q(u,\beta)h),0_6\right).

The lift is also where the two coordinate orderings meet. For the linearization below, record its constant derivative with respect to a physical GTSAM-ordered six-vector:

E=q(uβ)=[0I3I300000]R10×6.E=\frac{\partial q}{\partial(u-\beta)} =\begin{bmatrix} 0&I_3\\ I_3&0\\ 0&0\\ 0&0 \end{bmatrix} \in\mathbb R^{10\times6}.

This single matrix places angular rate and acceleration into Gal3’s internal (ω,ν,ρ,α)(\omega,\nu,\rho,\alpha) order. It is a derivative of the lift, not a second representation of the bias; the fixed zero position-rate and unit clock-rate coordinates have zero derivative.

The lifted state is propagated exactly as in EquivariantFilter::predictWithJacobian, by composing the increment on the right and then returning through the reference orbit:

Y+=YExpGL(λL(ξ,u)h),ξ+=ϕξ(Y+).Y^+=Y\operatorname{Exp}_{\mathcal G_L}(\lambda_L(\xi,u)h),\qquad \xi^+=\phi_{\xi^\circ}(Y^+).

For a left state action, this reconstruction equation is not ϕL(Exp(λh),ξ)\phi_L(\operatorname{Exp}(\lambda h),\xi), which would put the Galilean increment on the wrong side. The lift is invariant under the simultaneous state and input actions:

λL(ϕL(Y,ξ),ψL(Y,u))=λL(ξ,u).\lambda_L(\phi_L(Y,\xi),\psi_L(Y,u)) =\lambda_L(\xi,u).

Relation to Delama et al. Their construction closes its model over the full Galilean algebra: its input and bias coordinates lie in gal(3)\mathfrak{gal}(3), including virtual coordinates, and its symmetry is the semidirect product GR=Gal(3)gal(3)\mathcal G_R=\mathrm{Gal}(3)\ltimes\mathfrak{gal}(3). It uses the right actions

>ϕR((Υ,b),(C,γ))>=(ΥC,AdC1(bγ)),>ψR((w,τ),(C,γ))>=(AdC1(wγ),AdC1τ).>> \phi_R((\Upsilon,b),(C,\gamma)) > =(\Upsilon C,\operatorname{Ad}_{C^{-1}}(b-\gamma)),\qquad > \psi_R((w,\tau),(C,\gamma)) > =(\operatorname{Ad}_{C^{-1}}(w-\gamma),\operatorname{Ad}_{C^{-1}}\tau). >

That coherent pairing induces the invariant error XX^1X\hat X^{-1}. The adjoint in their input action does not preserve the six-dimensional physical IMU subspace by itself, which motivates completing the input and bias to the full algebra. The direct-product construction used here needs no adjoint transport: it is closed on the physical R6\mathbb R^6 input and bias spaces, while the lift supplies the fixed Gal3 coordinates. The group and both actions change together; this is not merely a reversal of the error.

6. Equivariant error and left-invariant discrete dynamics

For a left action, the EqF error is obtained by acting on the true state with the inverse group estimate:

ek=ϕL(Y^k1,ξk)=(Υ^k1Υk,  βkβ^k).\boxed{\mathsf e_k=\phi_L(\hat Y_k^{-1},\xi_k)} =\left(\hat\Upsilon_k^{-1}\Upsilon_k,\;\beta_k-\hat\beta_k\right).

This error is invariant under the common left action: replacing ξ\xi by ϕL(Y,ξ)\phi_L(Y,\xi) and Y^\hat Y by YY^Y\hat Y leaves e\mathsf e unchanged. Because the reference orbit is the identity, its normal coordinates are simply

ϵk=(ek,δβk)R16,ek=Log(Υ^k1Υk),δβk=βkβ^k.\epsilon_k=(e_k,\delta\beta_k)\in\mathbb R^{16},\qquad e_k=\operatorname{Log}(\hat\Upsilon_k^{-1}\Upsilon_k),\qquad \delta\beta_k=\beta_k-\hat\beta_k.

These are exactly the error coordinates in which EquivariantFilter::errorCovariance() is interpreted: a tangent vector at the fixed reference ξ\xi^\circ. The navigation component is also exactly GTSAM’s right-retracted local coordinate.

Exact discrete linearization

Adopt the six-dimensional measurement convention

u~k=uk+ηk,ηk=(ηa,k,ηω,k)R6.\tilde u_k=u_k+\eta_k,\qquad \eta_k=(\eta_{a,k},\eta_{\omega,k})\in\mathbb R^6.

The true bias is constant during this preintegration step. For one nominal increment define

x=q(u~,β^)h,V=Exp(x),C=JR(x)hER10×6.x=q(\tilde u,\hat\beta)h,\qquad V=\operatorname{Exp}(x),\qquad C=J_R(x)hE\in\mathbb R^{10\times6}.

The true corrected physical input is u~β^δβη\tilde u-\hat\beta-\delta\beta-\eta. By the definition of the complete Galilean right Jacobian, its increment satisfies

Vtrue=Exp(q(u~η,β^+δβ)h)VExp(C(δβ+η)).V_{\mathrm{true}} =\operatorname{Exp}\left(q(\tilde u-\eta,\hat\beta+\delta\beta)h\right) \simeq V\operatorname{Exp}(-C(\delta\beta+\eta)).

Write E=Υ^1Υ=Exp(e)\mathcal E=\hat\Upsilon^{-1}\Upsilon=\operatorname{Exp}(e). After one mean update,

E+=V1EVtrue.\mathcal E^+=V^{-1}\mathcal E V_{\mathrm{true}}.

Transporting the old right perturbation through VV and retaining first-order terms gives

e+=AdV1eCδβCη,δβ+=δβ.e^+=\operatorname{Ad}_{V^{-1}}e-C\delta\beta-C\eta,\qquad \delta\beta^+=\delta\beta.

Therefore, with ϵ=(e,δβ)R16\epsilon=(e,\delta\beta)\in\mathbb R^{16},

ϵ+=ALIϵ+BLIη,\boxed{\epsilon^+=A_{\mathrm{LI}}\epsilon+B_{\mathrm{LI}}\eta},

where

ALI=[AdV1C06×10I6]R16×16,BLI=[C06×6]R16×6.\boxed{ A_{\mathrm{LI}}= \begin{bmatrix} \operatorname{Ad}_{V^{-1}}&-C\\ 0_{6\times10}&I_6 \end{bmatrix} \in\mathbb R^{16\times16},\qquad B_{\mathrm{LI}}= \begin{bmatrix} -C\\ 0_{6\times6} \end{bmatrix} \in\mathbb R^{16\times6}.}

The upper-left block is not generally the identity: AdV1\operatorname{Ad}_{V^{-1}} transports a body/local error through the next increment. The upper-right block maps the physical GTSAM-ordered bias error directly into Gal3 coordinates through CC; no virtual bias columns are introduced.

Using the generic filter template literally. For an input held constant during the sample, the continuous-time error matrix is

>Ac=[adqE00],>Exp(Ach)=ALI.>> A_c=\begin{bmatrix}-\operatorname{ad}_{q^\circ}&-E\\0&0\end{bmatrix}, > \qquad \operatorname{Exp}(A_c h)=A_{\mathrm{LI}}. >

The current EquivariantFilter automatic path rejects left actions because Dϕ0DλD\phi_0D\lambda alone omits the adq-\operatorname{ad}_{q^\circ} transport associated with a left action and a right-composed prediction. A literal filter implementation must call predictWithJacobian with the continuous-time AcA_c; it must not pass the already-discrete ALIA_{\mathrm{LI}}, which that method would discretize again. The GalileanImuFactor itself does not instantiate EquivariantFilter; the template terminology specifies the geometry used by its preintegration.

Covariance propagation

Let ΣkR16×16\Sigma_k\in\mathbb R^{16\times16} be the covariance of (e,δβ)(e,\delta\beta). In GTSAM order, the continuous-time IMU noise density is

Qu=diag(Qa,Qω)R6×6,Qd=1hQu,Q_u=\operatorname{diag}(Q_a,Q_\omega)\in\mathbb R^{6\times6}, \qquad Q_d=\frac{1}{h}Q_u,

where QdQ_d is the covariance of the sampled rate noise. The first-order augmented propagation is

Σk+1=ALIΣkALI+BLIQdBLI.\boxed{ \Sigma_{k+1}=A_{\mathrm{LI}}\Sigma_kA_{\mathrm{LI}}^\top +B_{\mathrm{LI}}Q_dB_{\mathrm{LI}}^\top}.

Only physical accelerometer and gyroscope noise enters this model. If a caller already supplies per-sample rather than continuous-time covariances, the 1/h1/h conversion must not be applied a second time.

The three-way IMU factor does not itself constrain temporal bias evolution. It uses the navigation uncertainty conditioned on the bias linearization value, while a separate bias between-factor supplies the chosen random-walk model. Therefore initialize δβ=0\delta\beta=0 and propagate the conditional 10×1010\times10 navigation covariance directly:

Σe,k+1=AdVk1Σe,kAdVk1+CkQuhCk.\boxed{ \Sigma_{e,k+1}=\operatorname{Ad}_{V_k^{-1}}\Sigma_{e,k}\operatorname{Ad}_{V_k^{-1}}^\top +C_k\frac{Q_u}{h}C_k^\top}.

The deterministic 10×610\times6 bias sensitivity derived next accounts for changing the optimizer’s bias away from its linearization value; it is not part of this conditional covariance. A combined-bias factor could extend the input with a physical six-dimensional bias random walk and retain the full augmented covariance, without introducing virtual coordinates.

The augmented covariance above belongs to the direct-product, left-action EqF error used in GTSAM. It must not be identified directly with Delama et al.'s semidirect-product covariance, whose navigation and bias components use different frames and coordinates. For the conditioned navigation covariance consumed by the ordinary factor, the descriptions agree after the corresponding frame conversion.

7. Bias correction must also act on the right

Preintegration is performed once at a linearization bias β^=(b^a,b^ω)\hat\beta=(\hat b_a,\hat b_\omega) in GTSAM order. When optimization proposes β^+δβ\hat\beta+\delta\beta, recomputing every IMU sample would be wasteful. Define JkR10×6J_k\in\mathbb R^{10\times6} directly by

Υk(β^+δβ)Υ^k(β^)Exp(Jkδβ).\Upsilon_k(\hat\beta+\delta\beta) \simeq\hat\Upsilon_k(\hat\beta) \operatorname{Exp}(J_k\delta\beta).

This correction is on the right, matching the retraction convention. For Vk=Exp(xk)V_k=\operatorname{Exp}(x_k) and Ck=JR(xk)hER10×6C_k=J_R(x_k)hE\in\mathbb R^{10\times6}, transport of the old correction through the new increment gives

Jk+1=AdVk1JkCk,Ji=0.\boxed{J_{k+1}=\operatorname{Ad}_{V_k^{-1}}J_k-C_k}, \qquad J_i=0.

The minus sign follows from differentiating u~β\tilde u-\beta. The same recursion is obtained by accumulating the upper-right block of ALIA_{\mathrm{LI}}: the direct-product EqF transition exposes the physical bias-to-navigation sensitivity directly. Apply it as

Υk(β)Υ^kExp(Jk(ββ^)).\Upsilon_k(\beta)\simeq \hat\Upsilon_k \operatorname{Exp} \left(J_k(\beta-\hat\beta)\right).

The six columns of JkJ_k are ordered (ba,bω)(b_a,b_\omega), exactly like ConstantBias::vector(). There is no intermediate 10×1010\times10 virtual-bias Jacobian and no final column permutation. For the standard Galilean PIM, bias evolution remains the responsibility of a separate factor.

8. From the Galilean uncertainty to a GTSAM factor

The factor consumes a nine-dimensional NavState residual rather than the ten-dimensional Gal3 local error. In their respective tangent orderings,

e=(δθ,δv,δp,δt)R10,n=(δθ,δp,δv)R9.e=(\delta\theta,\delta v,\delta p,\delta t)\in\mathbb R^{10},\qquad n=(\delta\theta,\delta p,\delta v)\in\mathbb R^9.

Define the required selection and permutation when constructing the factor:

n=PNe,PN=[I300000I300I300]R9×10.n=P_Ne,\qquad P_N= \begin{bmatrix} I_3&0&0&0\\ 0&0&I_3&0\\ 0&I_3&0&0 \end{bmatrix} \in\mathbb R^{9\times10}.

The final column removes elapsed time, which is known exactly from the measurement timestamps. This projection applies to local errors, covariances, and Jacobians. The mean Galilean element is converted separately to NavState as (R,p,v)(R,p,v).

Let ΣeR10×10\Sigma_e\in\mathbb R^{10\times10} be the conditional Galilean covariance propagated above. The factor’s NavState residual covariance is

ΣN=PNΣePNR9×9.\Sigma_N=P_N\Sigma_eP_N^\top\in\mathbb R^{9\times9}.

Likewise, the first-order bias sensitivity of the local navigation error is

JN=PNJkR9×6.J_N=P_NJ_k\in\mathbb R^{9\times6}.

For the mean, first apply the Galilean bias correction and then construct

ΔXij=NavState(ΔRij,Δpij,Δvij).\Delta X_{ij}= \operatorname{NavState} (\Delta R_{ij},\Delta p_{ij},\Delta v_{ij}).

The known gravity and elapsed-time terms combine this delta with XiX_i to form predictedState_j. The residual used by the existing GTSAM preintegration machinery is

rij=LocalNavState(Xj,predictedStatej)=Xj.localCoordinates(predictedStatej).r_{ij}=\operatorname{Local}_{\mathrm{NavState}} (X_j,\operatorname{predictedState}_j) =X_j.\texttt{localCoordinates}(\operatorname{predictedState}_j).

In the C++ interface this is NavState::localCoordinates; the argument order above is intentional because GTSAM’s IMU residual asks how the measured state retracts to the prediction.

Conceptually this is a three-variable relation (Xi,Xj,βi)(X_i,X_j,\beta_i). The GalileanImuFactor alias uses ImuFactorT, so each NavState is exposed as separate pose and velocity keys and the concrete graph factor has five keys. In both views there is one bias variable for the entire interval. Bias evolution remains a separate factor, exactly as for the standard ImuFactor.

When bias evolution and state--bias correlation should be modeled inside the IMU factor, use PreintegratedCombinedMeasurementsG with GalileanCombinedImuFactor. The Combined PIM retains the same Galilean mean while propagating a public 15×1515\times15 covariance over (R,p,v,ba,bω)(R,p,v,b_a,b_\omega). The Gal(3) clock coordinate is deterministic and is projected out exactly. The resulting six-way factor adds the six bias-random-walk residuals to the nine navigation residuals.

9. Exact prediction in a rotating navigation frame

Let ωn\omega_n be the constant angular velocity of the navigation frame, expressed in navigation-frame coordinates, and let Ω=[ωn]×\Omega=[\omega_n]_\times. In the (R,v,p)(R,v,p) display order, the continuous dynamics are

R˙=ΩR+R[ω]×,v˙=g+Ra2ΩvΩ2p,p˙=v.\dot R=-\Omega R+R[\omega]_\times,\qquad \dot v=g+Ra-2\Omega v-\Omega^2p,\qquad \dot p=v.

GTSAM stores the same state as NavState(R,p,v). Define the mutually inverse transported-velocity maps in that order by

LΩ(R,p,v)=(R,p,vˉ),vˉ=v+Ωp,PΩ(R,p,vˉ)=(R,p,v),v=vˉΩp.\mathcal L_\Omega(R,p,v)=(R,p,\bar v),\quad \bar v=v+\Omega p,\qquad \mathcal P_\Omega(R,p,\bar v)=(R,p,v),\quad v=\bar v-\Omega p.

For θij=ωnΔtij\theta_{ij}=-\omega_n\Delta t_{ij}, form the exact SO(3)SO(3) kernels

Aij=Exp(θij),Gijv=JL(θij),Gijp=JL(θij)Γ2(θij),A_{ij}=\operatorname{Exp}(\theta_{ij}),\qquad G^v_{ij}=J_L(\theta_{ij}),\qquad G^p_{ij}=J_L(\theta_{ij})-\Gamma_2(\theta_{ij}),

and the rotating-frame world increment, now written directly in NavState order,

WijΩ=NavState(Aij,GijpgΔtij2,GijvgΔtij).W^\Omega_{ij}=\operatorname{NavState}\left( A_{ij},G^p_{ij}g\Delta t_{ij}^2,G^v_{ij}g\Delta t_{ij} \right).

The complete endpoint prediction preserves the same left-linear backbone:

Xj=PΩ ⁣(WijΩϕΔtij(LΩ(Xi))Uij(βi)).\boxed{X_j=\mathcal P_\Omega\!\left( W^\Omega_{ij}\,\phi_{\Delta t_{ij}}(\mathcal L_\Omega(X_i))\,U_{ij}(\beta_i) \right)}.

Writing Uij=(ΔR,Δp,Δv)U_{ij}=(\Delta R,\Delta p,\Delta v) and WijΩ=(A,pW,vW)W^\Omega_{ij}=(A,p_W,v_W) in GTSAM order makes the implementation explicit:

Rj=ARiΔR,pj=pW+A(pi+vˉiΔt+RiΔp),R_j=AR_i\Delta R,\qquad p_j=p_W+A(p_i+\bar v_i\Delta t+R_i\Delta p),
vˉj=vW+A(vˉi+RiΔv),vj=vˉjΩpj.\bar v_j=v_W+A(\bar v_i+R_i\Delta v),\qquad v_j=\bar v_j-\Omega p_j.

This is an exact constant-rate rotating-frame transition: Coriolis and centrifugal terms arise from the lift, group composition, and projection rather than an appended acceleration correction. The body-frame increment UijU_{ij}, its covariance, and its bias correction are unchanged; only endpoint prediction and its Jacobians depend on ωn\omega_n. In Python, enable this path with params.setOmegaCoriolis(omega_n). Omitting it (or setting exactly zero) selects the inertial prediction.

10. Intended use

A user-facing workflow mirrors the existing IMU factors:

  1. Construct preintegration parameters and a PreintegratedImuMeasurementsG at the current bias estimate. When the navigation frame rotates, call setOmegaCoriolis with its constant angular velocity in navigation-frame coordinates.

  2. Integrate each accelerometer/gyroscope sample. The public API and all six-dimensional quantities use GTSAM order (a,ω)(a,\omega); the lift places them in Gal3 order internally. Each call advances the Galilean mean on the right and propagates its conditional covariance.

  3. Construct a GalileanImuFactor between the two pose/velocity pairs and the interval’s bias key. During optimization, use the right-applied first-order bias correction rather than reintegrating immediately.

  4. Add a separate factor between consecutive bias keys when bias random-walk evolution is part of the model. Alternatively, construct PreintegratedCombinedMeasurementsG and a GalileanCombinedImuFactor to model the second bias key, its random walk, and state--bias correlation inside one factor.

In schematic C++ form:

PreintegratedImuMeasurementsG pim(params, biasHat);
for (const ImuSample& sample : samples) {
  pim.integrateMeasurement(sample.acceleration, sample.angularRate,
                           sample.deltaT);
}
graph.emplace_shared<GalileanImuFactor>(
    X(i), V(i), X(j), V(j), B(i), pim);

The Combined alternative has the corresponding form:

PreintegratedCombinedMeasurementsG combinedPim(combinedParams, biasHat);
// Integrate the same samples as above.
graph.emplace_shared<GalileanCombinedImuFactor>(
    X(i), V(i), X(j), V(j), B(i), B(j), combinedPim);

The API is deliberately familiar; the essential differences are internal geometric choices. The executable Python example below follows the same sequence. It predicts a consistent endpoint only to demonstrate factor construction; in a real graph, XjX_j is an optimizer variable rather than the prediction itself.

params = gtsam.PreintegrationParams.MakeSharedD(9.81)
params.setAccelerometerCovariance(1e-4 * np.eye(3))
params.setGyroscopeCovariance(1e-6 * np.eye(3))
params.setIntegrationCovariance(1e-8 * np.eye(3))

bias_hat = gtsam.imuBias.ConstantBias(
    np.array([0.01, -0.02, 0.03]),
    np.array([-0.01, 0.02, 0.01]),
)
pim = gtsam.PreintegratedImuMeasurementsG(params, bias_hat)

samples = [
    (np.array([0.2, -0.1, 9.7]), np.array([0.03, -0.02, 0.01]), 0.01),
    (np.array([0.1, 0.2, 9.8]), np.array([-0.01, 0.04, 0.02]), 0.02),
]
for measured_acc, measured_omega, delta_t in samples:
    pim.integrateMeasurement(measured_acc, measured_omega, delta_t)

print(f"Integrated {pim.deltaTij():.3f} s")
print("NavState residual covariance shape:", pim.preintMeasCov().shape)
Integrated 0.030 s
NavState residual covariance shape: (9, 9)
state_i = gtsam.NavState(
    gtsam.Rot3.RzRyRx(0.1, -0.2, 0.3),
    np.array([1.0, -2.0, 0.5]),
    np.array([0.4, -0.1, 0.2]),
)
state_j = pim.predict(state_i, bias_hat)

Xi, Vi = gtsam.symbol("x", 0), gtsam.symbol("v", 0)
Xj, Vj = gtsam.symbol("x", 1), gtsam.symbol("v", 1)
Bi = gtsam.symbol("b", 0)
factor = gtsam.GalileanImuFactor(Xi, Vi, Xj, Vj, Bi, pim)

graph = gtsam.NonlinearFactorGraph()
graph.add(factor)
error = factor.evaluateError(
    state_i.pose(), state_i.velocity(),
    state_j.pose(), state_j.velocity(), bias_hat,
)
print("Factor error at the predicted endpoint:", error)
Factor error at the predicted endpoint: [-5.69983774e-21 -4.35407115e-19  0.00000000e+00  0.00000000e+00
  0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
  0.00000000e+00]

References