tinyopt 700
Tinyopt, a lightweight, header only optimization library
Loading...
Searching...
No Matches
lm.h
1// Copyright 2026 Julien Michot.
2// SPDX-License-Identifier: Apache-2.0
3
4#pragma once
5
6#include <tinyopt/cost.h>
7#include <tinyopt/log.h>
8#include <tinyopt/solvers/gn.h>
9
10namespace tinyopt::lm {
11
12/***
13 * @brief Levenberg-Marquardt Solver Optimization options
14 *
15 ***/
16using SolverOptions = tinyopt::Options;
17} // namespace tinyopt::lm
18
19namespace tinyopt::solvers {
20
21template <typename Hessian_t = MatX>
22class SolverLM : public tinyopt::solvers::SolverGN<Hessian_t> {
23 public:
24 static constexpr bool IsNLLS = true;
25 static constexpr bool FirstOrder = false; // this is a pseudo second order algorithm
27 using Scalar = typename Hessian_t::Scalar;
28 static constexpr Index Dims = Base::Dims;
29
30 // Hessian Type
31 using H_t = Hessian_t;
32 // Gradient Type
33 using Grad_t = Vector<Scalar, Dims>;
34 // Options
35 using Options = tinyopt::Options;
36
37 explicit SolverLM(const Options &options = {}) : Base(options), options_{options} { reset(); }
38
40 void reset() override {
41 this->clear();
42 lambda_ = options_.lm.damping_init;
43 prev_lambda_ = 0;
44 bad_factor_ = options_.lm.bad_factor;
46 }
47
49 void Rebuild(bool b) override { rebuild_linear_system_ = b; }
50
53 template <typename X_t, typename AccFunc>
54 inline bool Build(const X_t &x, const AccFunc &acc_func, bool resize_and_clear = true) {
56 // Resize the system if needed and clear gradient
57 if (resize_and_clear) {
58 this->ResizeIfNeeded(x);
59 this->clear();
60 }
61
62 // Accumulate residuals and update both gardient and Hessian approx (Jt*J)
63 const bool success = this->Accumulate(x, acc_func);
64
65 // Early skip on failure (no residuals)
66 if (!success) {
67 if (options_.log.enable)
68 TINYOPT_LOG("❌ Failed to accumulate residuals: {}", this->cost().toString());
69 return false;
70 }
71
72 // Eventually clip the gradient
73 this->Clamp(this->grad_, options_.opt.grad_clipping);
74
75 // Verify Hessian's diagonal
76 if (options_.hessian.check_min_H_diag > 0 &&
77 (this->H_.diagonal().cwiseAbs().array() < options_.hessian.check_min_H_diag).any()) {
78 if (options_.log.enable) TINYOPT_LOG("❌ Hessian has very low diagonal coefficients");
79 return false;
80 }
81
82 // Fill the lower part if H if needed
83 if (!options_.hessian.H_is_full && RequiresFullMatrix(options_.linear_solver))
84 CompleteSymmetricMatrix(this->H_);
85
86 if (options_.lm.jacobi_scaling) ApplyJacobiScaling();
87
88 } else { // Keeping H and gradient, only evaluate the cost again
89
90 this->Evaluate(x, acc_func, true);
91 const bool success = this->cost().isValid();
92 // Early skip on failure (no residuals)
93 if (!success) {
94 if (options_.log.enable) TINYOPT_LOG("❌ Failed to accumulate residuals");
95 return false;
96 }
97 }
98
99 // Damping the diagonal: d' = d + lambda*d
100 if (lambda_ > 0.0) {
101 const double s =
102 rebuild_linear_system_ ? 1.0 + lambda_ : (1.0 + lambda_) / (1.0 + prev_lambda_);
103 for (int i = 0; i < this->H_.rows(); ++i) {
104 if constexpr (traits::is_matrix_or_array_v<H_t>)
105 this->H_(i, i) *= s;
106 else
107 this->H_.coeffRef(i, i) *= s;
108 }
109 }
110
111#if defined(TINYOPT_ENABLE_SUITESPARSE)
112 if constexpr (traits::is_sparse_matrix_v<H_t>) {
113 if (options_.linear_solver == LinearSolverMethod::SuiteSparse) this->H_.makeCompressed();
114 }
115#endif
116
117 return true;
118 }
119
121 void GoodStep(Scalar quality) override {
122 Scalar s = options_.lm.good_factor; // Scale to apply on damping lambda
123
124 // Use an approximative scaling based on the step quality TODO: improve this
125 if (quality != Scalar(0.0)) {
126 s = std::max<Scalar>(s, 1.0f - std::pow(2.0f * quality - 1.0f, 3.0f));
127 }
128
129 // Check whether the previous 'bad' step was actually good and revert the last scaling
130 if (bad_factor_ != options_.lm.bad_factor) s /= bad_factor_;
131
133 lambda_ =
134 std::clamp<Scalar>(lambda_ * s, options_.lm.damping_range[0], options_.lm.damping_range[1]);
135 bad_factor_ = options_.lm.bad_factor;
136 }
137
139 void BadStep(Scalar /*quality*/ = 0.0f) override {
140 Scalar s = bad_factor_; // Scale to apply on damping lambda
142 lambda_ =
143 std::clamp<Scalar>(lambda_ * s, options_.lm.damping_range[0], options_.lm.damping_range[1]);
144 bad_factor_ *= options_.lm.bad_factor;
145 }
146
148 void FailedStep() override { BadStep(); }
149
150 std::optional<Grad_t> Solve() const override {
151 auto delta = Base::Solve();
152 if (delta && options_.lm.jacobi_scaling) delta->array() *= scaling_.array();
153 return delta;
154 }
155
156 std::string stateAsString() const override {
157 std::ostringstream oss;
158 oss << TINYOPT_FORMAT_NS::format("○:{:.2e} ", 1.0 / lambda_);
159 return oss.str();
160 }
161
163 H_t Hessian() const {
164 H_t H = this->H_;
165 if (prev_lambda_ > 0.0) {
166 const Scalar s = 1.0f + prev_lambda_;
167 for (int i = 0; i < this->H_.cols(); ++i) {
168 if constexpr (traits::is_matrix_or_array_v<H_t>)
169 H(i, i) /= s;
170 else
171 H.coeffRef(i, i) = this->H_.coeff(i, i) / s;
172 }
173 }
174 if (options_.lm.jacobi_scaling) ApplyJacobiScaling(H, true);
175 return H;
176 }
177
179 std::optional<H_t> Covariance() const override { return InvCov(Hessian()); }
180
183 Scalar MaxStdDev(bool use_damped = true) const {
184 H_t H = use_damped ? this->H_ : Hessian();
185 if (use_damped && options_.lm.jacobi_scaling) ApplyJacobiScaling(H, true);
186 const auto I = InvCov(H);
187 if (!I) return 0;
188 using std::sqrt;
189 if constexpr (traits::is_sparse_matrix_v<H_t>)
190 return sqrt(I.value().coeffs().maxCoeff());
191 else
192 return sqrt(I.value().maxCoeff());
193 }
194
195 protected:
196 void ApplyJacobiScaling() {
197 constexpr Scalar MinDiagonal = Scalar(1e-6);
198 constexpr Scalar MaxDiagonal = Scalar(1e32);
199 scaling_.resize(this->H_.rows());
200 for (Index index = 0; index < this->H_.rows(); ++index) {
201 const Scalar diagonal = std::clamp(this->H_.coeff(index, index), MinDiagonal, MaxDiagonal);
202 scaling_[index] = Scalar(1) / std::sqrt(diagonal);
203 }
204 this->grad_.array() *= scaling_.array();
205 ApplyJacobiScaling(this->H_, false);
206 }
207
208 void ApplyJacobiScaling(H_t &hessian, bool inverse) const {
209 const auto scale_entry = [&](auto &value, Index row, Index column) {
210 const Scalar factor = scaling_[row] * scaling_[column];
211 if (inverse)
212 value /= factor;
213 else
214 value *= factor;
215 };
216 if constexpr (traits::is_sparse_matrix_v<H_t>) {
217 for (Index outer = 0; outer < hessian.outerSize(); ++outer) {
218 for (typename H_t::InnerIterator entry(hessian, outer); entry; ++entry)
219 scale_entry(entry.valueRef(), entry.row(), entry.col());
220 }
221 } else {
222 for (Index column = 0; column < hessian.cols(); ++column)
223 for (Index row = 0; row < hessian.rows(); ++row)
224 scale_entry(hessian(row, column), row, column);
225 }
226 }
227
228 const Options options_;
229 Grad_t scaling_;
230 Scalar lambda_ = 1e-4f;
231 Scalar prev_lambda_ = 0.0f;
232 Scalar bad_factor_ = 2.0f;
235};
236
237} // namespace tinyopt::solvers
bool Clamp(Grad_t &g, Scalar minmax) const
Clamp the gradient 'g' to within [-minmax, minmax], if minmax is not 0. Returns true if 'g' was clamp...
Definition base.h:30
Definition gn.h:16
bool Accumulate(const X_t &x, const AccFunc &acc)
Definition gn.h:104
Scalar Evaluate(const X_t &x, const AccFunc &acc, bool save)
Definition gn.h:93
bool ResizeIfNeeded(const X_t &x)
Definition gn.h:80
std::optional< Vector< Scalar, Dims > > Solve() const override
Definition gn.h:148
Scalar lambda_
Definition lm.h:230
Scalar bad_factor_
Definition lm.h:232
std::optional< H_t > Covariance() const override
Latest Covariance estimate.
Definition lm.h:179
void FailedStep() override
Damping stategy for a failure to solve the linear system, decrease the damping factor \lambda.
Definition lm.h:148
bool Build(const X_t &x, const AccFunc &acc_func, bool resize_and_clear=true)
Definition lm.h:54
void BadStep(Scalar=0.0f) override
Damping stategy for a bad step: decrease the damping factor \lambda.
Definition lm.h:139
Scalar MaxStdDev(bool use_damped=true) const
Definition lm.h:183
void GoodStep(Scalar quality) override
Damping stategy for a good step: increase the damping factor \lambda.
Definition lm.h:121
Scalar prev_lambda_
Definition lm.h:231
std::optional< Grad_t > Solve() const override
Solve the linear system dx = -H^-1 * grad, returns nullopt on failure.
Definition lm.h:150
void Rebuild(bool b) override
Force the solver to rebuild or skip it.
Definition lm.h:49
bool rebuild_linear_system_
Definition lm.h:233
H_t Hessian() const
Latest Hessian approximation (JtJ), un-damped.
Definition lm.h:163
void reset() override
Reset the solver state and clear gradient & hessian.
Definition lm.h:40
Levenberg-Marquardt specific solver, optimizer and their options.
Definition lm.h:13
Definition options.h:18