Skip to content

Numerical Thresholds Across Arithmetic Types

The pipeline was written and tuned in double. Every constant that sits next to a rounding error, such as a small-angle cut-off or a ridge on a normal matrix, was chosen against double’s unit roundoff ε=2.2×10−16\varepsilon = 2.2\times10^{-16}. When the same code runs in float, in a posit, or in a format built into hardware, those constants can stop doing their job. This page records which constants are sensitive, why, what PR #512 did about each, and what was measured. It is the starting point for anyone choosing a number format for a hardware implementation of these stages.

ConstantWhereKindAfter PR #512
small-angle hand-off, 1e-5math/lie/so3.hpp, sim3.hpp (θ)tolerance (a choice between two error sources)max⁡(10−5, εT1/3)\max(10^{-5},\ \varepsilon_T^{1/3}) via lie::detail::small_angle<T>()
scale-rate hand-off, 1e-5math/lie/sim3.hpp (σ integrals)tolerance, but no single value worksseries summed to convergence for ∣σ∣<1\lvert\sigma\rvert < 1, closed form above
PSD check slack, 1e-9sdk/msckf/dense.hpp is_positive_semidefinitetolerancefloored at 16 εT max⁡iPii16\,\varepsilon_T\,\max_i P_{ii} (a norm of P) via math::at_least_roundoff
up-vector degeneracy, 1e-9sdk/imu_init.hpp align_to_world_uptolerance on a sine in [0, 1]floored at 16 εT16\,\varepsilon_T
alignment ridge, 1e-9sdk/imu_init.hpp solve_alignmentregularizerfixed 1e-9 in every T
gyro-bias ridge, 1e-9sdk/imu_init.hppregularizerfixed 1e-9 in every T
triangulation ridge, 1e-9sdk/msckf/camera_updater.hppregularizerfixed 1e-9 in every T

For T = double:

  • small_angle returns 1e-5 exactly.
  • at_least_roundoff returns the old 1e-9 unless the entry it is scaled by exceeds about 2.8×1052.8\times10^5, where 16 ε ∣scale∣16\,\varepsilon\,\lvert\text{scale}\rvert passes 1e-9. That depends on the input, so it is not a guarantee.

As measured, the bench snapshot (132 outputs) and the EuRoC V1_01, MH_05 and V2_03 trajectories are unchanged bit for bit.

SO(3)‘s left Jacobian is

Jl(ϕ)=I+a(θ) W+b(θ) W2,a=1−cos⁡θθ2,b=θ−sin⁡θθ3,J_l(\phi) = I + a(\theta)\,W + b(\theta)\,W^2,\qquad a = \frac{1-\cos\theta}{\theta^2},\quad b = \frac{\theta-\sin\theta}{\theta^3},

with W=[ϕ]×W = [\phi]_\times and θ=∥ϕ∥\theta = \lVert\phi\rVert. Two ways of evaluating it fail in opposite directions as θ→0\theta \to 0.

  • The closed form cancels. 1−cos⁡θ1-\cos\theta is a difference of two numbers near 1, so it carries an absolute rounding error of about εT\varepsilon_T, whatever its true size. After dividing by θ2\theta^2 and multiplying by WW (entries of order θ\theta), the error in JlJ_l is about εT/θ\varepsilon_T/\theta. It grows as θ\theta shrinks. In float, cos⁡θ\cos\theta rounds to exactly 1 once θ2/2\theta^2/2 is below half an ulp of 1, which happens for θ≲2.4×10−4\theta \lesssim 2.4\times10^{-4}. Then a=0a = 0 and JlJ_l silently loses its W/2W/2 term.
  • The series truncates. The code’s series Jl≈I+W/2+W2/6J_l \approx I + W/2 + W^2/6 drops terms of order θ3/24\theta^3/24. That error shrinks as θ\theta shrinks.

The best hand-off balances the two:

εTθ=θ324⟹θ⋆≈(24 εT)1/4.\frac{\varepsilon_T}{\theta} = \frac{\theta^3}{24} \quad\Longrightarrow\quad \theta^\star \approx (24\,\varepsilon_T)^{1/4}.

The model bounds rounding from above, so measured crossovers sit a little lower: about 2×10−42\times10^{-4} for double (model 2.7×10−42.7\times10^{-4}) and 2.5×10−22.5\times10^{-2} for float (model 4.1×10−24.1\times10^{-2}).

The exponential and logarithm also have small-angle branches. Their IEEE closed forms, sin⁡(θ/2)/θ\sin(\theta/2)/\theta and 2 atan2(∥v∥,w)/∥v∥2\,\mathrm{atan2}(\lVert v\rVert, w)/\lVert v\rVert, do not cancel, so in float and double they are accurate at any θ>0\theta > 0 and the branch only avoids 0/00/0. Posits are different (see below).

The maximum entry error of JlJ_l over θ∈[10−6,0.1]\theta \in [10^{-6}, 0.1], evaluated at the same T-rounded ϕ\phi. This table, and the Jl−1J_l^{-1} and exponential figures below, come from a one-off sweep that is not in the tree. It compared against aa and bb summed as Taylor series in long double, an exact reference at these angles. The regression test that guards these numbers (so3_jacobian_sweep_error in tests/math/lie_groups.cpp) compares against SO3<double> instead. For float and the posits, double is accurate enough to reproduce the bounds, but it cannot measure double’s own error.

TypeεT\varepsilon_T at 1hand-off 1e-5 (before)hand-off max⁡(10−5,ε1/3)\max(10^{-5}, \varepsilon^{1/3}) (#512)threshold used
double2.2e-16~1e-12~1e-12 (unchanged, bit for bit)1e-5
float1.2e-75.7e-51.8e-64.9e-3
posit⟨32,2⟩7.5e-91.8e-51.4e-72.0e-3
posit⟨16,1⟩2.4e-43.2e-3 (13 ulps)1.3e-4, its rounding floor6.3e-2

The inverse Jacobian Jl−1=I−W/2+c W2J_l^{-1} = I - W/2 + c\,W^2, with c=1/θ2−cot⁡(θ/2)/(2θ)c = 1/\theta^2 - \cot(\theta/2)/(2\theta), behaves differently. cc is a difference of two terms of size 1/θ21/\theta^2, so cc itself is inaccurate, but it multiplies W2W^2 (of size θ2\theta^2). In IEEE the error in Jl−1J_l^{-1} stays near ε\varepsilon at every angle: float measured at most 1.3e-7.

Why ε1/3\varepsilon^{1/3} and not θ⋆\theta^\star

Section titled “Why ε1/3\varepsilon^{1/3}ε1/3 and not θ⋆\theta^\starθ⋆”

cortex holds T = double bit for bit through every change of this kind (#448’s gate). θ⋆\theta^\star for double (2.7×10−42.7\times10^{-4}) is above 1e-5, so using it would move double’s results. ε1/3\varepsilon^{1/3} is below 1e-5 for double and long double (6.1×10−66.1\times10^{-6} and 4.8×10−74.8\times10^{-7}), so they keep 1e-5 exactly. For every narrower format it is above 1e-5, and there it bounds:

  • the series’ truncation error by ε/24\varepsilon/24;
  • the closed forms’ cancellation by ε2/3\varepsilon^{2/3}.

That is a 30× improvement in float and 130× in posit32, but not the optimum.

Tapered formats: ε\varepsilon at 1 is not the precision at θ\theta

Section titled “Tapered formats: ε\varepsilonε at 1 is not the precision at θ\thetaθ”

A posit has the most fraction bits near magnitude 1 and fewer as values move away from it. Small θ\theta, θ2\theta^2 and sin⁡θ\sin\theta, and large 1/θ21/\theta^2, are represented with less precision than εT\varepsilon_T suggests. At 3×10−43\times10^{-4}, posit⟨16,1⟩ has about 7 fraction bits. The consequences, measured:

  • The series stays safe. It adds small corrections to O(1) terms, so the corrections’ coarse precision barely matters.
  • The exponential’s closed form fails. sin⁡(θ/2)/θ\sin(\theta/2)/\theta divides two small numbers that were rounded separately: posit16 is off by 0.6 % at θ=3×10−4\theta = 3\times10^{-4}, where IEEE has no error at all. With the type-scaled threshold (6.3e-2), posit16 never evaluates it there.
  • The inverse Jacobian’s closed form degrades. posit32 is off by 1.3e-6 (about 170 ε) at θ=10−5\theta = 10^{-5}, because 1/θ2≈10101/\theta^2 \approx 10^{10} has few bits. The threshold (2.0e-3) keeps posit32 on the series there.

So for a tapered format, the threshold also guards formulas that are stable in IEEE.

Sim(3)‘s scale-rate integrals: when no single hand-off works

Section titled “Sim(3)‘s scale-rate integrals: when no single hand-off works”

Sim(3)‘s exponential needs Ik(σ)=∫01ukeσu duI_k(\sigma) = \int_0^1 u^k e^{\sigma u}\,du for k=0,1,2k = 0, 1, 2, where σ\sigma is the log-scale rate. The closed forms cancel, and more deeply as kk grows:

closed formnumerator sizerelative error
I0I_0(eσ−1)/σ(e^\sigma - 1)/\sigmaσ\sigma∼ε/σ\sim \varepsilon/\sigma
I1I_1(eσ(σ−1)+1)/σ2(e^\sigma(\sigma-1)+1)/\sigma^2σ2/2\sigma^2/2∼2ε/σ2\sim 2\varepsilon/\sigma^2
I2I_2(eσ(σ2−2σ+2)−2)/σ3(e^\sigma(\sigma^2-2\sigma+2)-2)/\sigma^3σ3/3\sigma^3/3∼6ε/σ3\sim 6\varepsilon/\sigma^3

The code used SO(3)‘s hand-off and a three-term series for all three, which is wrong for every type. CodeRabbit flagged it on PR #512, and a one-off sweep against the series summed in long double showed:

Arithmetic, hand-offworst I2I_2 error
double, 1e-5 (before)33 % at σ=10−5\sigma = 10^{-5} (3 % at 3×10−53\times10^{-5})
float, 1e-5 (before)100 % or more, up to 107×10^7\times at σ=3×10−5\sigma = 3\times10^{-5}
float, ε1/3\varepsilon^{1/3} (#512’s first version)42 % at σ=10−2\sigma = 10^{-2}
any type, series summed to convergence for ∣σ∣<1\lvert\sigma\rvert<1 (#512)about 1 ulp

The fix avoids choosing a threshold at all. For ∣σ∣<1\lvert\sigma\rvert < 1 the Taylor series Ik=∑nσn/(n! (n+k+1))I_k = \sum_n \sigma^n / (n!\,(n+k+1)) has no cancellation, and it is summed until a term no longer changes the sum: at most 18 terms in double and 11 in float. For ∣σ∣≥1\lvert\sigma\rvert \ge 1 the closed forms lose at most a few ulps; the worst case is σ=−1\sigma = -1, where I2I_2‘s numerator is 5/e−2≈−0.165/e - 2 \approx -0.16. The hand-off at 1 is the same for every type.

The improvement in Sim3’s exponential, translation error against long double (test Sim3 exp is accurate at small scale rates in every type):

  • double: 3.1×10−13→9.9×10−153.1\times10^{-13} \to 9.9\times10^{-15};
  • float: 3.4×10−7→4.6×10−83.4\times10^{-7} \to 4.6\times10^{-8}.

Double’s remainder comes from the θ\theta branch of the same function, whose closed forms B/θB/\theta and C/θ2C/\theta^2 cancel like SO(3)‘s Jacobian and still share small_angle. That branch has not been swept. Sim(3) is used by no pipeline stage today, so none of this moves a VIO result.

For hardware, this is the general pattern. When a family of formulas cancels to different orders, a convergent series below a type-independent cut-off is simpler and more accurate than a per-formula, per-format threshold. In fixed-function hardware, the series becomes a fixed-length FMA chain whose length is set by the format’s ε\varepsilon.

Three solves in the pipeline add a ridge λ\lambda to the diagonal of a normal matrix H=J⊤JH = J^\top J before the Cholesky factorization:

  • triangulation’s Gauss-Newton step;
  • the initializer’s gyro-bias solve;
  • the initializer’s velocity/gravity/scale alignment.

The ridge makes an unexcited direction factorable. It also changes the answer. With H=∑iσivivi⊤H = \sum_i \sigma_i v_i v_i^\top,

xλ=(H+λI)−1b=∑iσiσi+λ  vi⊤bσi vi,x_\lambda = (H+\lambda I)^{-1} b = \sum_i \frac{\sigma_i}{\sigma_i+\lambda}\;\frac{v_i^\top b}{\sigma_i}\,v_i,

so each component of the solution is shrunk by λ/(σi+λ)\lambda/(\sigma_i+\lambda). A ridge is harmless only if it is far below every eigenvalue that carries information. Those eigenvalues come from the motion and the geometry, not from the number format.

The audit (#449) flagged T(1e-9) as a constant that “vanishes in 16-bit types”, and the first version of #512 floored it at 16 εT16\,\varepsilon_T. The S1 SfM bench (sfm_two_view_pnp on the known-answer window, float) failed:

Alignment ridgefloat scale error (bench)tolerance
fixed 1e-9 (main, kept)0.88 % (double: 0.78 %)2.12 %
floored flat at 16 εfloat=1.9×10−616\,\varepsilon_{\text{float}} = 1.9\times10^{-6}17.2 %2.12 %
floored at 16 ulps of each diagonal entry24.8 %2.12 %

To see why, the alignment’s HH and bb were dumped from the double run and analysed offline.

Alignment passsmallest eigenvalues of HHκ(H)\kappa(H)
rough (gravity free)1.4e-12 (a genuine near-null mode), 3.1e-4, …2.9e12
refined (gravity magnitude pinned)5.5e-6 (its eigenvector includes the scale), 3.1e-4, …7.1e5

The refined pass’s weakest informative mode has σ=5.5×10−6\sigma = 5.5\times10^{-6}:

  • a ridge of 10−910^{-9} shrinks it by 1.8×10−41.8\times10^{-4};
  • a ridge of 1.9×10−61.9\times10^{-6} shrinks it by 26 %.

Replaying the three ridges on the dumped system reproduces the bench:

Ridgescale bias, exact arithmeticscale bias, float32 solve
fixed 1e-90.000 %0.55 %
flat 16 εfloat16\,\varepsilon_{\text{float}}17.2 %17.5 %
16 εfloat ∣Hii∣16\,\varepsilon_{\text{float}}\,\lvert H_{ii}\rvert28.6 %28.8 %

The exact-arithmetic column is the point. The bias is regularization, not rounding: it is there even with no rounding at all. Scaling the ridge with εT\varepsilon_T ties a property of the problem to a property of the format, and here that removed a quarter of the information about scale. So all three ridges stay at a fixed 1e-9.

Doesn’t 1e-9 vanish in a coarse type? Only where it isn’t needed. On a unit-scale diagonal it is absorbed (1+10−9=11 + 10^{-9} = 1 in float), but there the matrix is already factorable. The ridge exists for an (almost) zero diagonal, and there 1e-9 survives in every bench type.

Precision: conditioning, and why normal equations hurt narrow formats

Section titled “Precision: conditioning, and why normal equations hurt narrow formats”

Rounding moves the solution by about κ(H) εT\kappa(H)\,\varepsilon_T. That is the 0.55 % in the float32 column above, with κ=7×105\kappa = 7\times10^5. Forming H=J⊤JH = J^\top J squares the conditioning of the constraints: κ(H)=κ(J)2\kappa(H) = \kappa(J)^2, so κ(J)≈840\kappa(J) \approx 840 here. For a format whose κ(H) εT≳1\kappa(H)\,\varepsilon_T \gtrsim 1, the normal equations cannot resolve the scale at all. A QR or square-root solve of the stacked constraints works with κ(J) εT\kappa(J)\,\varepsilon_T instead, a square root less. That is an estimate from the standard bounds, not measured here.

Whether 1e-9 survives depends on the format’s dynamic range, not its precision:

Formatε\varepsilon at 1small_angle1e-9 rounds toκ(H) ε\kappa(H)\,\varepsilon at κ=7×105\kappa = 7\times10^5
double2.2e-161e-51e-91.6e-10
float1.2e-74.9e-31e-90.08
posit⟨32,2⟩7.5e-92.0e-31e-95.2e-3
posit⟨16,1⟩2.4e-46.3e-23.7e-9 (minpos)171
bfloat167.8e-30.201.0e-95,500
IEEE half9.8e-40.100680
posit⟨8,0⟩3.1e-20.310.01622,000
Q16.16 fixed point(absolute step 1.5e-5)n/a0n/a

What this means for a port:

  • Half and Q16.16 round the ridge to zero. In Q16.16 the scale mode’s eigenvalue (5.5e-6) is also below one step.
  • posit8 rounds it up to 0.016. That is about 3,000× the scale mode, which it would erase.
  • In all three, flooring or raising the ridge is the wrong fix. The problem has to be rescaled (equilibrated) so that its informative eigenvalues sit where the format has precision, or solved in a wider format.

Two 1e-9 constants really are tolerances, and they now scale with T through math::at_least_roundoff(nominal, scale) =max⁡(nominal, 16 εT ∣scale∣)= \max(\text{nominal},\ 16\,\varepsilon_T\,\lvert\text{scale}\rvert):

  • The PSD check’s slack (is_positive_semidefinite). This is how far below zero an eigenvalue may sit and still count as rounding. A covariance computed in T carries rounding of about εT ∥P∥\varepsilon_T\,\lVert P\rVert in every entry, and an exactly singular P (a clone copied from its pose) has its zero eigenvalue land near −εT ∥P∥-\varepsilon_T\,\lVert P\rVert. So the slack is floored at 16 εT max⁡iPii16\,\varepsilon_T\,\max_i P_{ii} on every diagonal entry. With a fixed 1e-9, float would reject an exactly singular PSD matrix, because 1+10−91 + 10^{-9} rounds to 11. A floor per entry, 16 εT ∣Pii∣16\,\varepsilon_T\,\lvert P_{ii}\rvert, was the first fix and was not enough: on a row whose variance is far below ∥P∥\lVert P\rVert the rounding comes from the large entries it is coupled to. An end-to-end float run of the estimator broke it within seven frames (λmin⁡≈−10−7∥P∥\lambda_{\min} \approx -10^{-7}\lVert P\rVert, diagonals spread 27×). Two tests cover this (the PSD check keeps its ridge in a coarse type, the PSD check measures rounding against the norm of P).
  • The up-vector degeneracy test (align_to_world_up). This compares s=∣u×z^∣s = \lvert u \times \hat z\rvert, a sine in [0, 1], against 1e-9. In float, ss carries about 10−710^{-7} of rounding, so a fixed 1e-9 would accept rounding noise as a rotation axis.

For double the floor exceeds 1e-9 only on entries above about 2.8×1052.8\times10^5, so double results are unchanged.

  1. Classify every constant as a tolerance or a regularizer before porting it. Tolerances scale with the format’s ε\varepsilon at the magnitude they are compared with. Regularizers stay problem-scaled.
  2. Fix thresholds per format at design time. small_angle<T>() is a tuning constant computed once per type. In hardware it is a per-format constant (see the table above). The Jacobian branches compare θ2\theta^2 against the squared constant, so they need no square root.
  3. For a new format, use θ⋆=(24ε)1/4\theta^\star = (24\varepsilon)^{1/4}, then measure. The bit-identity constraint that holds cortex at ε1/3\varepsilon^{1/3} does not bind a fresh hardware datapath. Verify θ⋆\theta^\star with the sweep below, because the model is an upper bound.
  4. Evaluate the series with an FMA chain (Horner form). The series is the well-conditioned branch, and fused operations keep it so.
  5. For a tapered format, check precision at the operating magnitude, not at 1. Divisions of two small or two large quantities lose the most.
  6. Prefer QR or square-root forms to normal equations in narrow formats. They need κ(J) ε≪1\kappa(J)\,\varepsilon \ll 1 instead of κ(J)2 ε≪1\kappa(J)^2\,\varepsilon \ll 1.
  7. Check the ridge’s representability. A format without the range for 1e-9 needs the problem rescaled, not a different ridge.
  • The SO(3) error bounds are a regression test in tests/math/lie_groups.cpp (SO3 small-angle hand-off scales with the arithmetic type). It sweeps θ∈[10−6,0.1]\theta \in [10^{-6}, 0.1] in float, posit32 and posit16 and holds the measured maxima above with about 2× headroom. The posit16 run is a few dozen scalar evaluations: cheap, despite emulation.

  • The tolerance floors are tested in tests/math/arithmetic_types.cpp (per type and per scale) and tests/sdk/msckf_state.cpp (the float PSD check).

  • The ridge regression is guarded by the S1 bench:

    Terminal window
    ./build/tools/s1_initialization_bench --types double,float --variant sfm_two_view_pnp
    # truth.sfm_scale_error 0.7803 2 % PASS (double, known-answer window)
    # truth.sfm_scale_error 0.8766 2.119 % PASS (float)
  • The eigen-analysis of the alignment system used a temporary dump of HH and bb in ImuInitializer::solve_alignment, run on the known-answer window, followed by an eigendecomposition and replayed solves in NumPy. The dump is not in the tree; re-adding it before the ridge loop is a few lines, if a new format needs the spectrum.