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_gradientreturns , not the right-hand side (linalg/mod.rs). Each optimizer negates it before forming the linear system.tests/linear_solver_contract.rspins 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
| Field | Default | Meaning |
|---|---|---|
linear_solver_type | SparseCholesky | Which backend solves the normal equations |
min_diagonal | 1e-10 | Conditioning floor |
max_iterations | 50 | Iteration cap (Ceres’s default) |
cost_tolerance | 1e-6 | Relative cost-change stop |
parameter_tolerance | 1e-8 | Relative step-size stop |
gradient_tolerance | 1e-10 | First-order optimality stop |
use_jacobi_scaling | false | Column equilibration |
max_condition_number | None | Optional conditioning guard |
min_cost_threshold | None | Early exit once |
compute_covariances | false | Populate SolverResult::covariances |
timeout | None | Wall-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
| Field | Default | Notes |
|---|---|---|
damping (λ₀) | 1e-4 | Ceres’s initial_trust_region_radius = 1e4 corresponds to λ = 1/radius |
damping_min / damping_max | 1e-12 / 1e12 | Clamp after every update |
damping_increase_factor | 10.0 | Marquardt only |
damping_decrease_factor | 0.3 | Marquardt only |
damping_nu (ν₀) | 2.0 | Nielsen only |
min_diagonal / max_diagonal | 1e-6 / 1e32 | The clamp on D |
min_relative_decrease | 1e-3 | Acceptance threshold on ρ |
good_step_quality | 0.75 | Marquardt only |
min_step_quality | 0.0 | Marquardt only |
max_consecutive_rejected_steps | 5 | Then StalledNoProgress |
max_iterations | 50 | Ceres’s default |
cost_tolerance | 1e-6 | |
parameter_tolerance | 1e-8 | |
gradient_tolerance | 1e-10 | |
use_jacobi_scaling | false | Off 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
| Field | Default | Notes |
|---|---|---|
trust_region_radius (Δ₀) | 1e4 | Ceres-style large initial radius |
trust_region_min / max | 1e-12 / 1e12 | Δ floor triggers TrustRegionRadiusTooSmall |
trust_region_increase_factor | 3.0 | The 3 in max(Δ, 3‖h‖) |
trust_region_decrease_factor | 0.5 | |
good_step_quality | 0.75 | Grow above this |
poor_step_quality | 0.25 | Shrink below this |
min_step_quality | 0.0 | Acceptance threshold |
use_jacobi_scaling | true | Elliptical trust region |
initial_mu / min_mu / max_mu | 1e-4 / 1e-8 / 1.0 | Adaptive regularisation for a singular |
mu_increase_factor | 10.0 | μ shrinks as μ / (0.5·factor) on success |
enable_step_reuse | true | See 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:
InvalidNumericalValues— any of new cost, step norm or gradient norm is NaN or infinite.Timeout— elapsed ≥timeout.MaxIterationsReached— iteration ≥max_iterations.
Only after an accepted step (a rejected step cannot terminate the solve):
GradientToleranceReached— .- From iteration 1 onward,
ParameterToleranceReached: — a relative test, so it scales with the parameter magnitude. - From iteration 1 onward,
CostToleranceReached: . MinCostThresholdReached— optional cutoff.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):
| Field | Meaning |
|---|---|
parameters | Final values |
status | The OptimizationStatus above |
initial_cost / final_cost | at start and end |
iterations | Outer iterations performed |
elapsed_time | Wall clock for the solve |
convergence_info | Per-iteration history, when recorded |
covariances | Per-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.