Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Optimizers

This chapter documents what the three optimizers in src/optimizer/ actually do — the equations as implemented, the defaults as coded, and the deviations from textbook forms with the reason the code gives for each. Every formula names the module it comes from.

The problem all three solve

Given residual blocks over manifold variables, Problem defines

Variables live on manifolds, so the step is applied with rather than addition: , with the per-group right-plus from apex-manifolds. The Jacobian is therefore taken with respect to the tangent space, and every matrix below has tangent dimension, not parameter dimension. An SE3 variable contributes 6 columns, not 7.

Two quantities recur:

H drops the term of the true Hessian, which is the Gauss-Newton approximation and is accurate when residuals are small or nearly linear (gauss_newton.rs).

Sign convention. LinearSolver::get_gradient returns , not the right-hand side (linalg/mod.rs). Each optimizer negates it before forming the linear system. tests/linear_solver_contract.rs pins this for every backend, because getting it backwards produces a step that ascends.


Gauss-Newton

optimizer/gauss_newton.rs. Solves the normal equations exactly and takes the full step, with no step-size control at all (gauss_newton.rs):

There is no damping, no trust region, and no line search. Convergence is quadratic near a solution where the Gauss-Newton approximation holds, and the method diverges when started far away or when is ill-conditioned. Use it when you already know the problem is well-conditioned and the initial guess is close; otherwise use LM.

Options

FieldDefaultMeaning
linear_solver_typeSparseCholeskyWhich backend solves the normal equations
min_diagonal1e-10Conditioning floor
max_iterations50Iteration cap (Ceres’s default)
cost_tolerance1e-6Relative cost-change stop
parameter_tolerance1e-8Relative step-size stop
gradient_tolerance1e-10First-order optimality stop
use_jacobi_scalingfalseColumn equilibration
max_condition_numberNoneOptional conditioning guard
min_cost_thresholdNoneEarly exit once
compute_covariancesfalsePopulate SolverResult::covariances
timeoutNoneWall-clock cap

The schur_* fields (schur_variant, schur_preconditioner, schur_cg_max_iterations, schur_cg_tolerance, schur_cg_q_tolerance) exist on all three configs and are forwarded to the linear solver — see Linear Solvers.


Levenberg–Marquardt

optimizer/levenberg_marquardt.rs. The default optimizer, and the one for_bundle_adjustment() presets.

The damped system

D is not the identity. It is the clamped diagonal of the Hessian (linalg/mod.rs):

This is Ceres’s LevenbergMarquardtStrategy: damping each column in proportion to its own curvature makes the step invariant to a rescaling of the parameters, which uniform is not. Damping::identity sets and recovers plain exactly.

The lower clamp matters: without it a column with no curvature would receive no damping at all, leaving that direction unconstrained.

Step quality

After solving, the step is scored by the gain ratio. Predicted reduction comes from the quadratic model (optimizer/mod.rs):

evaluated with the un-damped — hessian_vec_product is documented to never carry , because damping in the model would corrupt (linalg/mod.rs). Then (optimizer/mod.rs):

with two guards the textbook ratio does not have:

  • → . A negative predicted reduction means the model says the step increases cost, so it is rejected outright rather than producing a misleading positive ratio.
  • → if the actual reduction was positive, else .

The step is accepted when min_relative_decrease, default 1e-3 (levenberg_marquardt.rs), matching Ceres. This is not the textbook .

Damping update — two rules

Selected by damping_update (levenberg_marquardt.rs).

Nielsen (default, what Ceres uses). On an accepted step:

On a rejected step and , so consecutive failures escalate geometrically. Reads damping_nu; ignores damping_increase_factor, damping_decrease_factor, min_step_quality, good_step_quality.

Marquardt. Three bands:

A rejected step always increases whatever was — the code notes that leaving it unchanged would recompute the identical step next iteration and stall (levenberg_marquardt.rs). Afterwards is clamped to [damping_min, damping_max].

Options

FieldDefaultNotes
damping (λ₀)1e-4Ceres’s initial_trust_region_radius = 1e4 corresponds to λ = 1/radius
damping_min / damping_max1e-12 / 1e12Clamp after every update
damping_increase_factor10.0Marquardt only
damping_decrease_factor0.3Marquardt only
damping_nu (ν₀)2.0Nielsen only
min_diagonal / max_diagonal1e-6 / 1e32The clamp on D
min_relative_decrease1e-3Acceptance threshold on ρ
good_step_quality0.75Marquardt only
min_step_quality0.0Marquardt only
max_consecutive_rejected_steps5Then StalledNoProgress
max_iterations50Ceres’s default
cost_tolerance1e-6
parameter_tolerance1e-8
gradient_tolerance1e-10
use_jacobi_scalingfalseOff because it is incompatible with Schur block structure (levenberg_marquardt.rs)

Note the scaling default differs between optimizers: LM defaults to false, Dog Leg to true. Enable it for LM on mixed-scale problems solved with Cholesky or QR.

for_bundle_adjustment() preset

levenberg_marquardt.rs overrides: ImplicitSparseSchur linear solver, SchurJacobi preconditioner, λ₀ = 1e-3 (the plain default is 1e-4), max_iterations = 20, cost_tolerance = 1e-6, parameter_tolerance = 1e-8, gradient_tolerance = 1e-10. The solver choice is justified by the measured table in that comment; see Benchmarks.


Dog Leg

optimizer/dog_leg.rs. Powell’s dog leg over a spherical trust region of radius , interpolating between the steepest-descent and Gauss-Newton directions.

Cauchy point

The unconstrained minimiser along (dog_leg.rs):

with a guard: if then .

Step selection — three cases

From dog_leg.rs, given :

Case 1 — GN step fits. If , take (StepType::GaussNewton).

Case 2 — Cauchy point already outside. If , take the steepest-descent direction scaled to the boundary (StepType::SteepestDescent):

Case 3 — interpolate. Otherwise walk from toward to the boundary. With , solve , i.e. with

The implementation does not use the textbook quadratic root. It uses Ceres’s cancellation-avoiding branch (dog_leg.rs), with :

Choosing by the sign of avoids catastrophic cancellation in the subtraction. Fallbacks: when (geometrically impossible, handled anyway) or (degenerate ). Finally is clamped to and (StepType::DogLeg).

Trust-region update

dog_leg.rs. Growth is Ceres’s rule, not a fixed doubling:

and is left unchanged in between.

is measured in the scaled space when Jacobi scaling is on — which is Dog Leg’s default. The code documents why: the radius bounds the scaled step, so feeding the un-scaled norm would compare two different units and grow by an arbitrary factor (dog_leg.rs).

Step reuse

On a rejected step Dog Leg caches the GN step, Cauchy point, gradient and , and reuses them next iteration (dog_leg.rs). Only changed, so the expensive linear solve does not have to be redone. The cache is invalidated on any accepted step, because the parameters moved. Controlled by enable_step_reuse (default true).

Options

FieldDefaultNotes
trust_region_radius (Δ₀)1e4Ceres-style large initial radius
trust_region_min / max1e-12 / 1e12Δ floor triggers TrustRegionRadiusTooSmall
trust_region_increase_factor3.0The 3 in max(Δ, 3‖h‖)
trust_region_decrease_factor0.5
good_step_quality0.75Grow above this
poor_step_quality0.25Shrink below this
min_step_quality0.0Acceptance threshold
use_jacobi_scalingtrueElliptical trust region
initial_mu / min_mu / max_mu1e-4 / 1e-8 / 1.0Adaptive regularisation for a singular
mu_increase_factor10.0μ shrinks as μ / (0.5·factor) on success
enable_step_reusetrueSee above

Jacobi column scaling

When use_jacobi_scaling is on, columns are equilibrated before the solve (optimizer/mod.rs):

The solve happens in the scaled space and the step is mapped back with . The in the denominator keeps the scaling finite for a structurally empty column.

Ordering matters and the code is explicit about it: the predicted reduction is computed before un-scaling (levenberg_marquardt.rs), because the solver’s cached gradient and Hessian are the scaled ones and all three vectors must live in the same space. The predicted reduction is a value of the quadratic model and is invariant under the change of variables, so it is equally valid for the un-scaled step.


Convergence criteria

All three optimizers share check_convergence (optimizer/mod.rs), evaluated in this order.

Always checked, before anything else:

  1. InvalidNumericalValues — any of new cost, step norm or gradient norm is NaN or infinite.
  2. Timeout — elapsed ≥ timeout.
  3. MaxIterationsReached — iteration ≥ max_iterations.

Only after an accepted step (a rejected step cannot terminate the solve):

  1. GradientToleranceReached — .
  2. From iteration 1 onward, ParameterToleranceReached: — a relative test, so it scales with the parameter magnitude.
  3. From iteration 1 onward, CostToleranceReached: .
  4. MinCostThresholdReached — optional cutoff.
  5. TrustRegionRadiusTooSmall — LM and Dog Leg only.

LM additionally reports StalledNoProgress after max_consecutive_rejected_steps consecutive rejections (levenberg_marquardt.rs): damping has grown until every trial step is negligible, so the remaining budget cannot change the cost.

Output

All three return SolverResult<T> (optimizer/mod.rs):

FieldMeaning
parametersFinal values
statusThe OptimizationStatus above
initial_cost / final_cost at start and end
iterationsOuter iterations performed
elapsed_timeWall clock for the solve
convergence_infoPer-iteration history, when recorded
covariancesPer-variable tangent-space covariance; None unless compute_covariances

Note that several statuses are successful terminations, not failures: Converged, CostToleranceReached, GradientToleranceReached, ParameterToleranceReached and StalledNoProgress are all treated as converged by the benchmark harness. MaxIterationsReached means the budget ran out with the solve still improving.