IMUs — what you can buy, and what the numbers mean

An IMU is three gyros and three accelerometers in a box, and its price spans four orders of magnitude for the same nominal function. What you are actually buying is how slowly the errors grow. This page maps the market by grade, gives representative parts with representative numbers, and — most importantly — connects each datasheet line to the term it becomes in your navigation error budget.

New: a step-by-step build. If you want to implement a consumer for a navigation-grade delta-theta/delta-V unit rather than survey the market, the HG9900 tutorial walks the whole path — three output rates, half-frame increment alignment, the error-state $F$ and $Q$, the two-rate interlock and the feed-forward correction — with runnable, self-checking code.

Symbols used on this page are defined in the Nomenclature → inertial navigation.

Indicative public-datasheet values for orientation, not procurement. Theory and the error model itself are on the IMU & inertial navigation page.

The five numbers that decide everything

SpecUnitsWhat it does to your solution
Angle random walk (ARW)°/√hrWhite gyro noise. Attitude error grows as $\sigma\sqrt t$ — dominates in the first seconds after a fix.
Bias instability°/hrThe Allan-variance floor. An uncorrected bias $b$ tilts the gravity vector and gives position error $\propto \tfrac16 g\,b\,t^3$.
Turn-on repeatability°/hrHow much bias changes per power cycle — this is what your filter must estimate from scratch every start.
Velocity random walk (VRW)m/s/√hrAccelerometer white noise → velocity, then position, random walk.
Scale factor & misalignmentppm, mradProportional to motion: irrelevant when static, dominant during a high-rate slew or a long burn.
The dominance rule. Over a gap of duration $t$: attitude error from ARW grows like $t^{1/2}$, from bias like $t^{1}$, and the resulting position error from an uncorrected tilt like $t^{3}$. So short gaps are a noise problem and long gaps are a bias problem — which spec you should pay for depends entirely on how long you must fly without aiding.

Grades, with real drift consequences

GradeGyro biasARWAccel biasPosition drift, unaidedPrice band
Consumer MEMS100–1000 °/hr1–3 °/√hr10–50 mghundreds of m in ~10 s$1–20
Industrial MEMS3–30 °/hr0.2–1 °/√hr1–5 mg~100 m/min$100–3 k
Tactical0.5–3 °/hr0.05–0.2 °/√hr0.3–1 mg~1 nmi in 10 min$5 k–30 k
Navigation0.005–0.05 °/hr0.001–0.01 °/√hr10–50 µg~1 nmi/hr$50 k–200 k
Strategic< 0.001 °/hr< 0.0002 °/√hr< 1 µg< 0.1 nmi/hr$500 k+

The classic navigation-grade benchmark — 1 nautical mile per hour of unaided position drift — is not a marketing number; it falls out of a ~0.01 °/hr bias tilting gravity. That is why "navigation grade" has meant roughly the same thing for decades even as the underlying technology changed.

Representative parts

MEMS — consumer & industrial

PartGyro bias stab.ARWNotes
Bosch BMI088 / InvenSense ICM-42688~100–400 °/hr~1–2 °/√hrcents-to-dollars, drone/phone class; needs constant aiding
Analog Devices ADIS16505~2–8 °/hr~0.15–0.3 °/√hrindustrial workhorse, factory-calibrated over temperature, SPI
Analog Devices ADIS16547~0.8 °/hr~0.05 °/√hrtactical-adjacent MEMS, very good g-sensitivity
SBG Ellipse-N / VectorNav VN-200~5–10 °/hr~0.2 °/√hrintegrated GNSS/INS modules with an EKF onboard

FOG & RLG — tactical to navigation

PartTechnologyGyro bias stab.Notes
KVH DSP-1760 / 1775FOG~0.05–0.5 °/hrcompact fibre-optic, popular for ground robots & marine
Honeywell HG1700RLG~1 °/hrtactical standard, very widely fielded
Honeywell HG9900 / HG9848RLG + precision accel~0.003 °/hrnavigation grade, aircraft & launch vehicles
Northrop Grumman LN-200FOG~0.1–1 °/hrspace-qualified variants, long flight heritage
Safran / Sagem Sigma 40FOG (HRG variants)~0.001–0.01 °/hrnaval reference, strategic-adjacent
Safran / Northrop HRG (Regys, SciRS)Hemispherical resonator~0.0001–0.001 °/hrno moving parts to wear, superb long-life stability, space use

Space-grade variants of the same product line typically trade a factor of 2–5 in performance for radiation tolerance, and cost far more.

Choosing by mission, not by spec sheet

SituationDominant errorWhat to buy
Continuous RTK-GNSS, open skynone — aiding bounds everythingindustrial MEMS; spend the money on the receiver and antenna instead
Urban canyon, seconds-long outagesARW & VRW (short-term noise)low-ARW MEMS or small FOG; a great bias spec buys you little here
Minutes of GNSS denialbias instabilitytactical FOG/RLG, plus aiding from alt-PNT sources
Hours unaided (submarine, strategic)bias + scale factor over temperaturenavigation/strategic RLG or HRG; consider gravimetric/MagNav aiding
Spacecraft attitude with star trackersARW between tracker updateslow-ARW FOG or HRG; bias is observable and estimated by the MEKF
Launch vehiclescale factor + g-sensitivity under high accelnavigation grade with strong vibration-rectification performance

See the grade difference

The drift simulator makes the "which spec matters" argument concrete. Set the bias and noise to a grade from the table above, then change the interval between fixes: with frequent aiding almost any IMU works, and as the gap grows the error explodes in the $t^3$ way that only a better bias spec (or another aiding source) can fix:

Gyrocompassing is the other place hardware choice is decisive — a self-alignment to north is only possible if the gyro can resolve the horizontal component of Earth rate, $\Omega\cos(\text{lat})\le 15.04$ °/hr. A 100 °/hr consumer MEMS cannot see Earth rotating at all, so it can never find north on its own:

What the sensor actually ships

A navigation-grade IMU does not output acceleration and angular rate. It outputs integrals — a velocity increment $\Delta\mathbf v$ and an angle increment $\Delta\boldsymbol\theta$, each accumulated over a window:

$$ \Delta\mathbf v_k = \int_{t_{k-1}}^{t_k}\! \mathbf f^b(t)\,dt, \qquad \Delta\boldsymbol\theta_k = \int_{t_{k-1}}^{t_k}\! \boldsymbol\omega^b(t)\,dt. $$

That distinction is the root of everything in the next three sections. An increment is an interval quantity: it has a start time, an end time, and no single instant at which it is "true". A rate sample has an instant but throws away everything that happened between samples. For a strapdown system the increment is strictly better — it cannot alias, it is immune to sample-time jitter within the window, and summing increments loses nothing — but it means every downstream consumer has to know which window each number belongs to.

Three rates, not one: 1800 / 600 / 300 Hz

Inside the unit the sensors are read far faster than the data is published, and the published outputs do not all share a rate. A navigation-grade RLG unit such as the Honeywell HG9900 is a good worked example, because its three rates are related by simple integer decimation of one internal clock:

RateIntervalWhat lives at this rate
1800 Hz0.5556 ms Internal sub-sample. Never published. The unit integrates its gyros and accelerometers here, and this is the only place sub-interval structure exists
600 Hz1.6667 ms Angle increment. Three sub-samples summed. Two of these are published per output frame — $\Delta\theta_A$ and $\Delta\theta_B$
300 Hz3.3333 ms Output frame / RTI. Six sub-samples. One $\Delta V$ and the two $\Delta\theta$ halves are transmitted per real-time interrupt

So the angle increments are a 600 Hz stream delivered in 300 Hz packets, while the velocity increment is genuinely a single 300 Hz quantity. That asymmetry is the whole story:

1800 Hz sub-samples (0.5556 ms each); n = first sub-sample of frame k frame k-1 | frame k | 1 2 3 4 5 6 | 1 2 3 4 5 6 | +--------------------------------+--------------------------------+ ^ RTI (300 Hz real-time interrupt) d-theta_A = sum sub-samples n .. n+2 -> 600 Hz, 1.6667 ms, FIRST half of frame k d-theta_B = sum sub-samples n+3 .. n+5 -> 600 Hz, 1.6667 ms, SECOND half of frame k d-V = sum sub-samples n-3 .. n+2 -> 300 Hz, 3.3333 ms, SHIFTED BACK by 3 '--- three from frame k-1, three from frame k ---' => the d-V window sits half a frame BEHIND the two d-theta windows.

Note the sub-sample indices on the $\Delta V$ line: n−3 through n+2, not n through n+5. Three of the six come from the previous frame. That is not arbitrary — it is what an anti-alias filter costs. A symmetric FIR needs taps on both sides of its output epoch, so the filtered result is necessarily valid at an instant in the past.

The real sensor's lag is 4 sub-samples plus about 40 µs — roughly 2.262 ms, not an exact half frame. A simulation stepping at the 300 Hz base rate cannot represent a 4/1800 s shift without running everything at 1800 Hz, so a model will typically approximate it as a clean 3-sub-sample (half-frame) lag and drop the 40 µs. That is a reasonable simplification, but it is one: the residual is about 0.6 ms of unmodelled delay, which by the arithmetic in §time of validity is ~0.1 mrad of attitude mismatch at 10 °/s. Know whether you are reading the sensor's behaviour or a model's.

The 12-tap anti-alias filter spans the previous and current six sub-samples, with weights of the general shape $[1,3,5,7,9,11,11,9,7,5,3,1]/12$ — symmetric, which is what makes the group delay constant and the lag a clean half-frame in the idealised case. It is applied to $\Delta V$ and to the flight-control accelerometer and gyro outputs, and not to the two transmitted $\Delta\theta$ messages.

Why the angle increments are deliberately left unfiltered. Filtering them would blend the two 600 Hz halves together and destroy the one piece of sub-frame structure the interface carries. What that structure is for is covered in §why two halves — and it is not what most people assume.
The anti-alias filter is usually applied to $\Delta V$ and not to the transmitted $\Delta\boldsymbol\theta$. That asymmetry is deliberate — filtering the angle increments would destroy the sub-frame structure the mechanization needs — but it is also exactly why the two streams end up on different time bases. If you assume both are filtered the same way, your velocity will be wrong by the amount derived in the next section.

Time of validity — the error nobody budgets for

Each published quantity carries its own window. Writing them out for the architecture above, with the output frame running over $[t_0, t_1]$ and $t_{0.5}$ the midpoint:

OutputValid overNote
$\Delta\boldsymbol\theta_a$$[t_0,\ t_{0.5}]$ First half-frame, unfiltered
$\Delta\boldsymbol\theta_b$$[t_{0.5},\ t_1]$ Second half-frame, unfiltered
$\Delta\mathbf v$$[t_{-0.5},\ t_{0.5}]$ Lags half a frame — filter spans the boundary
Flight-control accel$[t_{-0.5},\ t_{0.5}]$ $\Delta\mathbf v / \Delta t$, same lag
Flight-control gyro$[t_{-0.5},\ t_{0.5}]$ Filtered, so it lags too — unlike the message $\Delta\boldsymbol\theta$
The consequence for the consumer. The $\Delta\mathbf v$ arriving in frame $k$ belongs to a window that ended at the midpoint of frame $k$. To rotate it into the navigation frame correctly you need the attitude over that window — which means the attitude propagated using the previous frame's second half-increment and this frame's first half. In practice the consumer caches $\Delta\boldsymbol\theta_b$ across the frame boundary. Skip the cache and you rotate a velocity increment by an attitude that is half a frame too new.

What the mismatch costs

If the increment is rotated by an attitude valid $\delta t$ away from the correct epoch, the residual attitude error is $\delta\theta = \omega\,\delta t$ and the induced velocity error per frame is at worst $\delta\theta\,|\Delta\mathbf v|$ — worst case being when the rotation axis is perpendicular to the increment. For a half-frame mismatch at 300 Hz ($\delta t = 1.67$ ms) under 1 g:

Body rateAttitude mismatchVelocity error rate After 100 s
1 °/s29 µrad0.29 mm/s per s0.03 m/s
10 °/s291 µrad2.9 mm/s per s0.29 m/s
60 °/s1745 µrad17 mm/s per s1.7 m/s

At 1 °/s this is a rounding error. At launch-vehicle pitch rates it is metres per second of velocity error over a burn, from nothing but a bookkeeping mistake about which window a number belonged to. The error is linear in both body rate and mismatch, and it is a bias, not noise — it does not average away.

This is also why the time-tagging note below matters so much: an uncertain epoch and a misattributed window are the same error with different causes. Both produce $\omega\,\delta t$.

Why Δθ is published as two half-frame sums

The obvious guess is "for the coning correction". That is wrong, and it is worth saying why, because the real reason is more useful.

Coning is already done, inside the unit, at 1800 Hz. The sensor has six sub-samples per frame and corrects non-commutativity using all of them. Two halves of a 300 Hz frame would be a far cruder approximation to the same thing. If the halves existed for coning they would be redundant with work the sensor has already completed — and worse, a consumer applying a coning term to already-corrected increments would be double-correcting.

The real reason: the velocity rotation needs a midpoint attitude

The accelerometers measure $\Delta\mathbf v$ in body axes, but it has to be accumulated in the navigation frame — and the body frame rotates during the interval. Which attitude should do the rotating?

ChoiceErrorOrder
Attitude at the start of the interval $\sim \Delta\boldsymbol\theta \times \Delta\mathbf v$First order
Attitude at the end of the interval $\sim \Delta\boldsymbol\theta \times \Delta\mathbf v$, opposite sign First order
Attitude at the midpoint The two first-order terms cancelSecond order

The midpoint is the right answer, and it is the midpoint rule — the same second-order trick as the trapezoid. But a midpoint attitude is only reachable if you can advance the running attitude by exactly half the interval's rotation. With a single full-frame $\Delta\boldsymbol\theta$ you cannot: you can go nowhere or all the way, and nothing in between.

That is what the halves buy. Apply $\Delta\boldsymbol\theta_A$ to reach the midpoint attitude, rotate $\Delta\mathbf v$ with it, then apply $\Delta\boldsymbol\theta_B$ to reach the end-of-frame attitude. The mechanization therefore carries two attitudes per cycle with different jobs: the midpoint one rotates velocity and is the linearisation point for the filter; the end one is the reported attitude and the start of the next interval.

This also captures most of the sculling effect structurally rather than as an explicit correction term — the velocity is being rotated by an attitude valid in the middle of the window over which it accumulated, which is exactly what sculling compensation is trying to achieve.

A corollary that catches people: the 600 Hz rate is not an invitation to run the attitude update at 600 Hz and the velocity update at 300 Hz. Both halves belong to one 300 Hz frame, and the $\Delta V$ for that frame is lagged relative to both of them (§time of validity). The halves are a structural subdivision for the midpoint rule, not a faster data stream.

Same packet, different epochs — and how a fixed-rate filter copes

Two questions that follow immediately from the timing above, and that get answered wrongly surprisingly often.

Are Δθ and ΔV published at different times?

They are transmitted at the same time and valid over different windows. One real-time interrupt at 300 Hz carries one $\Delta V$ and both $\Delta\theta$ halves in the same message. There is no staggered delivery. What differs is each quantity's time of validity: the angle halves cover $[t_0, t_{0.5}]$ and $[t_{0.5}, t_1]$ of the current frame, while the velocity increment covers $[t_{-0.5}, t_{0.5}]$ — half a frame earlier.

This is worth stating carefully because the two failure modes are opposite. If you assume they arrive at different times you will go looking for a synchronisation mechanism that does not exist. If you assume that arriving together means they are about the same interval, you commit the half-frame rotation error quantified in §time of validity. The arrival is simultaneous; the bookkeeping is not.

one RTI packet at 300 Hz contains: d-V valid over [t(-0.5), t(0.5)] <-- half a frame behind d-theta_A valid over [t(0), t(0.5)] d-theta_B valid over [t(0.5), t(1)] so the consumer must pair this frame's d-V with: the PREVIOUS frame's d-theta_B (covers t(-0.5) to t(0)) this frame's d-theta_A (covers t(0) to t(0.5)) '-------------- together these span the d-V window -------------'

Hence the cache. The consumer holds $\Delta\boldsymbol\theta_B$ across the frame boundary so that when the next $\Delta V$ arrives it can build an attitude valid over precisely that increment's window. In a layered flight-software stack this caching usually lives in the hardware-abstraction wrapper rather than the navigation algorithm, which is the right place for it — it is a property of the sensor's output contract, not of the estimator.

How does a fixed-rate EKF consume a 300 Hz increment stream?

It does not. That is the key structural point: the filter never sees the increments at all. There are two loops running at different rates with different jobs.

Fast loop (mechanization)Slow loop (EKF)
RateSensor rate — every packet, 300 Hz Much slower — typically 1–10 Hz
Consumes$\Delta V$, $\Delta\theta_A$, $\Delta\theta_B$ External measurements (GNSS, pad constraints, star tracker)
OwnsThe state: position, velocity, attitude The covariance and the error-state estimate
MathsNonlinear integration of the strapdown equations Linear propagation of $\delta\mathbf x$ and $P$
Never doesCompute a covarianceTouch an increment

The fast loop integrates every increment as it arrives and publishes a state. The slow loop needs the state-transition matrix $\Phi$ over the whole interval since it last ran — which may be hundreds of sensor frames. Two naive options are both bad: forming $\Phi$ once from a single snapshot of the dynamics ignores everything that happened in between, and multiplying hundreds of per-frame $\Phi_k$ matrices is expensive and numerically ugly.

The standard resolution is to accumulate, not to multiply. The fast loop sums the coupling terms that $\Phi$ is built from — specific force, the direction-cosine matrix, the lever arms — into buckets as it goes. When the filter runs, it averages each bucket over the frames it contains and forms one $\bar F$ for the interval, then one $\Phi \approx \exp(\bar F \Delta T)$. The filter sees a fixed-rate, well-conditioned propagation step even though the data feeding it arrived at 300 Hz.

So the answer to "how do they go through the EKF at a fixed rate" is that they do not go through it. They go through the mechanization at the sensor rate, and what reaches the filter is an interval-averaged summary at the filter's own cadence. The correction then flows the other way as a one-off state adjustment — feed-forward only, with the filter never driving the mechanization continuously.

One consequence worth internalising: because the filter's $\Delta T$ is set by its own schedule and not the sensor's, the increments' time-of-validity bookkeeping must be correct before anything is accumulated. A half-frame misattribution does not get averaged away by the bucketing — it is a bias, so it accumulates into $\bar F$ and then into $\Phi$, and the filter has no way to detect it. The full two-rate derivation, with the buffer and buckets written out, is in IMU theory & error model.

One full EKF cycle, written out

This is the question the previous section raises and does not answer: if the filter runs at 1–10 Hz and the increments arrive at 300 Hz, what exactly happens to the hundreds of increments that accumulated in between? Here is one complete cycle with concrete shapes.

The full derivation — bucket buffer, partial-span slivers at the filter boundaries, UD-factored updates, the control-vector hand-back — is in IMU theory §7. This section is the worked example with numbers, and the measured accuracy of the approximation.

Setup

QuantitySymbolValue in this example
IMU frame$\Delta t$1/300 s = 3.333 ms
Filter interval$\Delta T$1 s ⇒ 300 frames per cycle
Bucket span$\Delta t_j$0.1 s = 30 frames ⇒ 10 buckets
Error state$\delta\mathbf x$15 × 1
Covariance$P$15 × 15

The 15 error states, grouped: position $\delta\mathbf r$ (3), velocity $\delta\mathbf v$ (3), attitude $\delta\boldsymbol\psi$ (3), accelerometer bias $\delta\mathbf b_a$ (3), gyro bias $\delta\mathbf b_g$ (3).

Step 1 — the fast loop consumes every increment

At each frame $k$ the mechanization compensates the raw increments, integrates them, and additionally accumulates the terms the filter will need. Compensation first:

$$ \Delta\mathbf v^c_k = \big(I + \hat S_a + \hat M_a\big)^{-1} \big(\Delta\tilde{\mathbf v}_k - \hat{\mathbf b}_a \Delta t\big), \qquad \Delta\boldsymbol\theta^c_k = \big(I + \hat S_g + \hat M_g\big)^{-1} \big(\Delta\tilde{\boldsymbol\theta}_k - \hat{\mathbf b}_g \Delta t\big) $$

Then the midpoint attitude from §why two halves, the velocity rotation, and the state update:

$$ C^n_b\big|_{\text{mid}} = C^n_b\big|_{k-1} \oplus \Delta\boldsymbol\theta^c_A, \qquad \Delta\mathbf v^n_k = C^n_b\big|_{\text{mid}}\Delta\mathbf v^c_k - 2\boldsymbol\omega_{ie}\times\mathbf v^n \Delta t + \mathbf g^n \Delta t $$ $$ \mathbf v^n_k = \mathbf v^n_{k-1} + \Delta\mathbf v^n_k, \qquad \mathbf r^n_k = \mathbf r^n_{k-1} + \big(\mathbf v^n_{k-1} + \tfrac12\Delta\mathbf v^n_k\big)\Delta t $$
Here is the answer to "how do the accumulated increments enter the filter". They do not enter as increments. Each frame, the fast loop evaluates the error-state dynamics matrix $F_k$ — which is built from the same compensated increment, via the specific force $\mathbf f^n_k = \Delta\mathbf v^n_k / \Delta t$ and the current attitude — and adds it into a running bucket sum:
$$ \mathcal B_j \mathrel{+}= F_k, \qquad n_j \mathrel{+}= 1 $$

with the structure of $F$ for this state ordering being

$$ F = \begin{bmatrix} 0 & I & 0 & 0 & 0 \\ 0 & -2[\boldsymbol\Omega_{ie}\times] & -[\mathbf f^n\times] & C^n_b & 0 \\ 0 & 0 & -[\boldsymbol\omega^n_{in}\times] & 0 & -C^n_b \\ 0 & 0 & 0 & -\tfrac{1}{\tau_a}I & 0 \\ 0 & 0 & 0 & 0 & -\tfrac{1}{\tau_g}I \end{bmatrix} $$

The $-[\mathbf f^n\times]$ block is the tilt-to-velocity coupling — the reason a specific force has to be accumulated at all. It is what carries an attitude error into a velocity error, and it changes every frame as the vehicle manoeuvres. Accumulating $F_k$ is accumulating a summary of the increments, in exactly the form the filter needs.

Step 2 — the slow loop forms one Φ for 300 frames

When the filter runs, each bucket is normalised to a mean and exponentiated once, and the buckets are chained:

$$ \bar F_j = \frac{1}{n_j}\mathcal B_j, \qquad \Phi_j = \exp\!\big(\bar F_j \Delta t_j\big) \approx I + \bar F_j \Delta t_j + \tfrac12\big(\bar F_j \Delta t_j\big)^2 $$ $$ \Phi_m = \prod_{j=1}^{10} \Phi_j $$

So the cost is 10 matrix exponentials and 9 products per second, not 300 of each — and the 300 increments are all represented, through the sums they contributed to.

Step 3 — how good is that approximation?

This is an approximation and deserves a number rather than a reassurance. Every figure in this subsection is produced by ekf_two_rate.py (§the script), measured against the exact product $\prod_k \exp(F_k \Delta t)$ on the $F$ sequence the simulation actually generated — not estimated. Over a 1 s filter cycle at 300 Hz, for a vehicle pitching at 10 °/s under 1 g so that $F$ genuinely varies across the interval:

BucketsFrames per bucketRel. error in Φ (10 °/s, 1 g) vs a single averageRel. error (60 °/s, 4 g)
13002.16 × 10−2— 1.32 × 10−1
21505.43 × 10−34× 3.37 × 10−2
31002.41 × 10−39× 1.51 × 10−2
5608.88 × 10−424× 5.43 × 10−3
10302.36 × 10−4 91×1.36 × 10−3
30102.34 × 10−5922× 1.48 × 10−4
10032.20 × 10−5981× 1.20 × 10−5
30010 (exact)—0 (exact)
Relative error in the bucketed state-transition matrix against the exact per-frame
              product, log-log versus bucket count, for benign and aggressive dynamics, with a
              1/n-squared reference line.
Measured output of ekf_two_rate.py. Both cases track the $1/n^2$ reference over the useful range; the benign curve flattens past 30 buckets for the reason given below.
The error falls as the square of the bucket count — each 3× more buckets buys roughly 10× the accuracy, and the measured log-log slope is 2.00 across the middle of the range. Ten buckets per filter cycle gets within $2.4\times10^{-4}$ of the exact product for 3% of the matrix exponentials, which is the trade the design is making.
More buckets stops helping, and the reason is the sensor. Past about 30 buckets the benign curve flattens near $2\times10^{-5}$ instead of continuing down, and at 100 buckets it is marginally worse than at 30. Re-running the sweep with parts of the sensor model disabled isolates the cause: with quantization off the $1/n^2$ law continues cleanly to $1.9\times10^{-6}$, and with quantization on but all stochastic errors off the curve still flattens. So the floor is quantization ripple specifically. Because $\mathbf f^n$ is reconstructed as $C^n_b\,\Delta\mathbf v/\Delta t$ from a quantized $\Delta\mathbf v$, every frame's $F$ carries a ripple term that averaging 30 frames smooths but averaging 3 frames does not. The useful bucket count is bounded above by the sensor's own quantization, not just by the compute budget.

At 60 °/s and 4 g the 10-bucket error is $1.36\times10^{-3}$ — about six times worse than the benign case, which is the expected behaviour: the harder the manoeuvre, the more $F$ moves within a bucket. Note also that the aggressive case has not hit the ripple floor by 100 buckets, because its genuine variation still dominates the quantization term there. Bucket count is a dynamics-dependent tuning parameter, not a universal constant.

Step 4 — propagate, then update

One time update for the whole 300-frame span:

$$ \delta\mathbf x^-_m = \Phi_m\,\delta\mathbf x^+_{m-1} = \mathbf 0, \qquad P^-_m = \Phi_m P^+_{m-1} \Phi_m^{\mathsf T} + Q_m $$

The error state propagates to zero because the previous cycle's correction was already handed to the mechanization and the error state reset — the filter estimates the error remaining, and only $P$ carries memory across cycles. The process noise for the interval is

$$ Q_m = \int_0^{\Delta T}\! \Phi(\xi) G W G^{\mathsf T} \Phi(\xi)^{\mathsf T} d\xi \;\approx\; \operatorname{diag}\big( \sigma^2_{\text{VRW}},\, \sigma^2_{\text{ARW}},\, \tfrac{2\sigma^2_{b_a}}{\tau_a},\, \tfrac{2\sigma^2_{b_g}}{\tau_g}\big)\,\Delta T $$

then each measurement is applied as an independent scalar update, in a fixed order:

$$ \text{for each scalar } i: \quad K_i = \frac{P\,\mathbf h_i^{\mathsf T}}{\mathbf h_i P \mathbf h_i^{\mathsf T} + R_i}, \qquad \delta\mathbf x \mathrel{+}= K_i\big(z_i - \mathbf h_i \delta\mathbf x\big), \qquad P \leftarrow (I - K_i \mathbf h_i) P $$
The measurement is stale the moment it is used. A GNSS fix is valid at its own epoch $t_{\text{meas}}$, which is neither the filter boundary nor an IMU frame boundary. The residual must be formed against the mechanized state at that epoch — which is why the fast loop keeps a ring buffer of past states rather than just the latest one. Differencing a fix against the current state instead of the contemporaneous one reintroduces exactly the time-of-validity error from §time of validity, at the scale of the measurement latency rather than half a frame.

Step 5 — hand the correction back

$$ \mathbf c_m = -\,\delta\hat{\mathbf x}^+_m, \qquad t(\mathbf c_m) = t_m $$

The mechanization applies the position, velocity and attitude parts at the buffered state for epoch $t_m$ and then re-integrates forward to the present, because by the time the filter finished, the fast loop has already run on past it. The bias and scale-factor parts go into the compensation of Step 1 for all subsequent frames, which is what closes the loop: a better bias estimate makes the next 300 increments cleaner, which makes the next $\bar F$ more accurate.

one 1 s cycle at 300 Hz / 1 Hz k=1..30 fast: compensate, mechanize, accumulate F into bucket 1 k=31..60 fast: ... bucket 2 ... k=271..300 fast: ... bucket 10 <- 300 state updates, 0 covariance work filter tick: Fbar_j = B_j / n_j for j = 1..10 Phi_j = expm(Fbar_j * 0.1) Phi_m = Phi_10 ... Phi_1 <- 10 expm, 9 products P- = Phi_m P+ Phi_m' + Q_m <- ONE time update per-scalar measurement updates <- residuals vs BUFFERED states c_m = -dx+ <- correction, stamped at t_m k=301.. fast: apply c_m at buffered t_m, re-integrate to now, then carry on with improved bias estimates

Two asymmetries are worth noticing in that listing. The fast loop does no covariance arithmetic — no 15×15 matrix is touched 300 times a second. And the filter does no integration: it never sees a $\Delta V$ or a $\Delta\theta$, only the accumulated $F$ sums they produced and the buffered states they generated. That division is what makes a navigation-grade filter affordable on flight hardware.

Running it: a verified implementation

Everything above is implemented and checked in a single self-contained script. It synthesises IMU outputs with the three-rate structure of §increments — 1800 Hz sub-samples, two 600 Hz angle increments, one 300 Hz $\Delta V$ shifted back by three sub-samples — applies the documented error model per sub-sample, and runs the two-rate filter exactly as §one full EKF cycle describes.

⬇ ekf_two_rate.py View source

python3 ekf_two_rate.py # full run, verification and figures python3 ekf_two_rate.py --selftest # verification only, about 9 s python3 ekf_two_rate.py --no-plots # numbers only python3 ekf_two_rate.py --duration 120 --mc 14 --mc-duration 60 requires numpy; matplotlib only for the figures

What it generates

The truth trajectory is defined by a body-rate and specific-force profile and then integrated, so the ideal increments are exact integrals of that profile rather than finite differences of a sampled path. On top of the ideal increments, per sub-sample, in this order:

TermFormApplied to
FOGM bias $x_k = (1-\beta\,dt)\,x_{k-1} + \sqrt{2\sigma^2\beta}\;n\sqrt{dt}$ both, as $x_k\,dt$
Turn-on biasdrawn once per power cycleboth, as $b_0\,dt$
Scale factorstatic + FOGM, multiplies the increment both — generated, never estimated
Misalignment / non-orthogonalityoff-diagonal $M$, multiplies the increment both
Random walk$\sigma_{\text{rw}}\,n\sqrt{dt}$ (VRW, ARW)both
Quantization$\lfloor \cdot/q \rfloor q$ with residual carry both; gyro dither added before the floor, excluded from the carry
Anti-alias filtersymmetric 12-tap over previous + current sub-samples $\Delta V$ only — never the angle increments

Error magnitudes are representative navigation-grade figures (0.0035 °/hr gyro bias, 25 µg accelerometer bias, 5 ppm and 100 ppm scale factor) of the right order for the grade rather than any particular part's specification. The quantization steps are round illustrative LSBs for the same reason.

How it verifies itself

A simulation that merely runs proves nothing, so each claim is a check that can fail. The most useful of them is the error-free reproduction test: with every error source, the quantizer and the anti-alias filter disabled, a perfect sensor must reproduce the truth trajectory. It is run before any other result is reported, and it earned its place — it caught two real bugs in the half-frame bookkeeping during development (see the callout below).

CheckWhat would fail itMeasured
Anti-alias centroida $\Delta V$ epoch that is not the frame boundary $t_k + 0.0000$ ms; taps sum to exactly 6
Zero-rotation controlan error in the translation integration 6.5 × 10−12 m
Algorithm faithfulnesscache, epoch or midpoint-rotation mistakes 0.000 × 100 (bit-identical)
vs 1800 Hz truth— irreducible, see below 5.3 µm, 2.4 µm/s, 1.8 × 10−15 rad
Midpoint rulerotating $\Delta V$ by the wrong attitude 4.1 × 10−7 m/s, vs 8.6 mm/s at the frame start
Φ bucketinga bucket count too low for the dynamics slope 2.00; 2.4 × 10−4 at 10 buckets
Filter consistencyan optimistic covariance NIS 1.017 over 420 updates, interval [0.865, 1.135]
Two bugs the error-free test caught, both in the half-frame bookkeeping. It first reported 3 mm of position and 3 × 10−4 rad of attitude error, which is small enough to look like discretization — but attitude should be exact to machine precision within a frame, because the rotation axis is constant there. It was not discretization. Frame 0 cannot be mechanized at all: its $\Delta V$ window reaches three sub-samples back before $t_0$, which do not exist. So the first usable frame is frame 1, and the state entering it is valid at $\Delta t/2$, not at $0$ — initialising at $0$ quietly swallows half a frame of motion. And $\Delta\boldsymbol\theta_B(0)$ is still perfectly good and must prime the cache even though its frame is discarded. After both fixes, attitude went to 1.8 × 10−15 rad and the independent-integration check to exactly zero.
The residual against 1800 Hz truth is not a bug, and the script says so. It survives because the published outputs are 300 Hz block sums that cannot express sub-frame motion — it is the price of the interface, not of the consumer. Two measurements establish that: a zero-rotation run is exact to 10−12, and re-integrating the same ideal increments with an independent copy of the same algorithm agrees bit for bit. What remains is only the 300 Hz discretization, and it disappears only if the sensor publishes data it never publishes.

Results

A 120 s run at 300 Hz — 36,000 mechanized frames from 216,000 synthesised sub-samples, with a 1 Hz GNSS position fix — settles to the following steady-state RMS errors over the second half:

QuantitySteady-state RMSNote
Position2.07 magainst a 1.5 m per-axis fix
Velocity0.054 m/snever measured directly
Tilt146 µradobservable through the specific force
Heading64 µradobservable only while manoeuvring
Accelerometer bias25.4 µgweakly observable in the vertical
Gyro bias0.0046 °/hrcomparable to the turn-on spec
Five stacked time histories over 120 seconds: position, velocity and attitude error each
              against their own per-axis one-sigma envelopes, then accelerometer and gyro bias estimates
              tracking their true values.
Error against the filter's own per-axis $1\sigma$ envelope, and the bias states converging. Velocity and attitude sit inside their envelopes; the vertical channel is the loosest, which is expected — it is constrained only by the position fix, and accelerometer bias is weakly observable there.

One plotting bug worth naming, because it is easy to ship: the first version drew a single $\sigma$ band for all three axes of each group. Heading $\sigma$ is an order of magnitude larger than tilt $\sigma$, so that made the heading channel look divergent when it was not. Each axis now carries its own envelope.

The time-of-validity experiment

The script runs the same measurements through two otherwise identical filters: one forms the residual against the buffered state at the fix epoch, the other against the latest state. With the fix valid 0.37 s before the filter boundary:

Run lengthResidual vs buffered stateResidual vs latest state Ratio
12 s1.50 m5.98 m4.0×
30 s0.86 m88.5 m103×
60 s0.74 m268 m360×
120 s2.08 m642 m309×
This is divergence, not a penalty — and the distinction matters. A single number from a 12 s run would have read "4× worse" and sounded like a tolerable degradation. It is not: the buffered column is flat while the latest column grows without bound, which is the signature of a feedback loop being driven rather than a one-off error. The mechanism is that at a 0.37 s offset the vehicle has moved metres, differencing against the current state attributes all of that motion to position error, the filter corrects it, the correction is fed forward — and the next cycle sees the same systematic offset again. The error ratchets instead of averaging out. This is why the fast loop keeps a ring buffer of past states and not merely the latest one.

Filter consistency, and what the 15-state vector leaves out

Scale factor, misalignment and non-orthogonality are generated but never estimated — a realistic choice, and one whose cost is measured rather than hidden. Normalised innovation squared (NIS) should average 1 for a consistent filter; over 60 s runs:

ConfigurationMean NISVerdict
As shipped, all error terms active1.017consistent
Scale factor removed from the sensor1.010consistent
Perfect sensor0.999consistent

So un-modelled sensor error accounts for about +0.019 of the excess and scale factor alone for +0.008. A real filter closes that gap either by putting scale factor in the state vector or by inflating $Q$ deliberately to cover it.

A test that cannot move its own output is not evidence. An earlier attempt to explain a 13% NIS excess scaled $Q$ by up to 100× and saw no change at all, which looked like proof that $Q$ was irrelevant. It proved nothing: $Q$'s position entry is $\sim\!10^{-6}$ m² against a $P_{rr}$ of order 1 m², so even 100× is negligible against it. The excess was then decomposed properly by removing one error term at a time, which also showed that most of what remained was an initial-condition transient — it fell from 1.091 to 1.063 to 1.035 as the run lengthened from 30 s to 60 s, and reaches 0.999 once the discarded first half covers the transient.

Coning and sculling — and who is responsible for them

These two effects come from the same fact: rotations do not commute, and over a finite interval the attitude is changing while you integrate.

Coning

If two body axes oscillate with a phase difference, the body traces a cone and accumulates a real, secular rotation about the third axis. Naively summing the angle increments as if they were vectors misses it entirely, because vector addition is commutative and rotation is not. The first-order correction restores the missing term:

$$ \Delta\boldsymbol\theta^{\text{corrected}} = \Delta\boldsymbol\theta_a + \Delta\boldsymbol\theta_b + \underbrace{\tfrac{1}{2}\,\Delta\boldsymbol\theta_a \times \Delta\boldsymbol\theta_b}_{\text{coning}}. $$

The cross product of the two half-increments recovers part of what a single full-frame sum throws away. It is worth being careful about what that does and does not explain — see the callout immediately below. For a sinusoidal two-axis oscillation of amplitude $A$ at angular frequency $\omega$, in quadrature, the drift rate of the uncorrected sum is

$$ \dot\theta_{\text{coning}} \approx \tfrac{1}{2} A^2 \omega . $$

That quadratic dependence on amplitude is brutal. Evaluated numerically at 5 Hz:

AmplitudeClosed form $A^2\omega/2$Numeric
1°0.274 °/s0.274 °/s
3°2.47 °/s2.46 °/s
6°9.87 °/s9.79 °/s

Tripling the amplitude multiplies the drift by nine. A 6° quadrature oscillation at 5 Hz — well within what airframe flex or a vibrating mount can produce — makes an uncorrected attitude solution useless within seconds.

Sculling

The velocity analogue. If the body rotates while it accelerates, and the two oscillate in phase, the product rectifies into a steady velocity error. The correction has the same shape:

$$ \Delta\mathbf v^{\text{corrected}} = \Delta\mathbf v + \underbrace{\tfrac{1}{2}\,\Delta\boldsymbol\theta \times \Delta\mathbf v}_{\text{rotation}} + \underbrace{\tfrac{1}{2}\big(\Delta\boldsymbol\theta_a \times \Delta\mathbf v_b + \Delta\mathbf v_a \times \Delta\boldsymbol\theta_b\big)}_{\text{sculling}} . $$
Phase is what decides whether sculling exists at all. With the angular and linear oscillations in phase the integrand goes as $\sin^2\omega t$, whose mean is $\tfrac12$ — so the error accumulates secularly. Put them 90° apart and the integrand becomes $\tfrac12\sin 2\omega t$, which averages to zero over whole cycles and the effect vanishes. I had the phase wrong on the first attempt at measuring this and got a clean 0.00000 m/s, which is the right answer to the wrong question.

Measured for a 2° oscillation against 5 m/s² in phase at 5 Hz, comparing a fine integration against what a consumer can reconstruct from block sums alone:

Output intervalVelocity error rateOver 10 s
3.33 ms (300 Hz)0.08 mm/s per s0.8 mm/s
10 ms (100 Hz)0.72 mm/s per s7.2 mm/s
20 ms (50 Hz)2.8 mm/s per s28 mm/s

The error scales roughly as $\Delta t^2$, which is the practical argument for a high output rate: going from 50 Hz to 300 Hz cuts the uncorrected sculling error by a factor of ~35.

Where the correction actually happens

This is the part most often got wrong, and it is a hardware question rather than a software one.

EffectNeedsWho can do it
ConingSub-interval angular history Inside the IMU. Only the unit sees its own internal samples
ScullingSub-interval angular and linear history Inside the IMU, same reason
Velocity-frame rotationAttitude at the increment's midpoint Navigation software — it owns the attitude
Window alignmentKnowing each output's time of validity Navigation software — and it must cache across frames
Coning and sculling compensation that needs information finer than the output interval must be done by the sensor, because that information never crosses the interface. Once the unit has published a block sum, the sub-interval detail is gone and no amount of downstream cleverness recovers it. What the navigation software can and must do is rotate each increment using an attitude valid at the right epoch, and keep straight which window each number came from. Those are different problems, and conflating them is how a navigation system ends up with a correction applied twice or not at all.

A lower-grade unit that reports only sampled rate and acceleration shifts the burden the other way: with no sub-interval information published at all, the consumer has to sample fast enough that coning and sculling are negligible, which is usually not achievable. That is the real reason increment output is worth paying for.

Integration details that cost you accuracy

  • Time-tagging. If the IMU's sample epoch is uncertain by $\delta t$ while rotating at $\omega$, you inject an attitude error $\omega\,\delta t$ — at 100 °/s a 1 ms uncertainty is 0.1°, larger than everything else in the budget. Use the hardware sync/PPS pin, not the arrival time of the packet.
  • Which window, not just which instant. Per time of validity above, each output has its own interval and $\Delta\mathbf v$ typically lags the angle increments. Align them before mechanizing, and cache across the frame boundary if the unit publishes a split $\Delta\boldsymbol\theta$.
  • Rate vs increment output. Prefer $\Delta\theta/\Delta v$ (integrated increments) over sampled rate: increments are inherently coning/sculling-friendly and immune to sample-time jitter.
  • Mounting and thermal path. The lever arm from IMU to the GNSS antenna must be measured to the millimetre; thermal gradients across the block show up directly as bias.
  • Vibration. Check the vibration-rectification-error spec, not just the noise floor — MEMS can turn a symmetric vibration into a DC acceleration bias.

Planned bench work

  • Multi-hour static logs → Allan-variance curves per axis, compared to the datasheet.
  • Turn-on repeatability over dozens of power cycles and a temperature sweep.
  • Free-inertial drift race between grades on the same trajectory.
  • Gyrocompassing convergence time vs latitude and averaging window.

Related: IMU theory & error model · gyroscope technologies · alt-PNT · back to Hardware.