tinyopt 700
Tinyopt, a lightweight, header only optimization library
Loading...
Searching...
No Matches
bfgs.h
1// Copyright 2026 Julien Michot.
2// SPDX-License-Identifier: Apache-2.0
3
4#pragma once
5
6#include <algorithm>
7#include <array>
8#include <cmath>
9#include <limits>
10
11#include <tinyopt/solvers/gd.h>
12
13namespace tinyopt::solvers {
14
15template <typename Gradient_t = VecX>
16class SolverBFGS : public SolverGD<Gradient_t> {
17 public:
18 using Base = SolverGD<Gradient_t>;
19 using Scalar = typename Base::Scalar;
20 using Grad_t = typename Base::Grad_t;
21 using Matrix_t = Matrix<Scalar, Base::Dims, Base::Dims>;
22 static constexpr Index Dims = Base::Dims;
23 static constexpr bool IsNLLS = false;
24 static constexpr bool FirstOrder = true;
25
26 explicit SolverBFGS(const Options &options = {}) : Base(options), step_size_(options.bfgs.step_size) {}
27
28 void reset() {
30 step_size_ = this->options_.bfgs.step_size;
31 matrix_initialized_ = false;
32 has_gradient_ = false;
33 has_step_ = false;
34 update_available_ = false;
35 }
36
37 template <typename X_t, typename AccFunc>
38 bool Build(const X_t &x, const AccFunc &acc, bool resize_and_clear = true) {
39 if constexpr (Dims == Dynamic) this->ResizeIfNeeded(x);
40 InitializeWorkspace();
41
42 const bool can_update = has_gradient_ && has_step_;
43 if (can_update) {
44 update_step_ = last_step_;
45 update_previous_gradient_ = this->grad_;
46 }
47
48 if (!Base::Build(x, acc, resize_and_clear)) {
49 has_gradient_ = false;
50 update_available_ = false;
51 return false;
52 }
53
54 update_available_ = can_update;
55 if (can_update) update_gradient_delta_ = this->grad_ - update_previous_gradient_;
56 has_gradient_ = true;
57 has_step_ = false;
58 return true;
59 }
60
61 std::optional<Vector<Scalar, Dims>> Solve() const override {
62 if (!this->cost().isValid()) return std::nullopt;
63
64 Vector<Scalar, Dims> direction = -(inverse_hessian_ * this->grad_).eval();
65 if (!direction.allFinite() || this->grad_.dot(direction) >= 0) {
66 inverse_hessian_.setIdentity();
67 direction = -this->grad_;
68 }
69
70 last_step_ = step_size_ * direction;
71 has_step_ = true;
72 return last_step_;
73 }
74
75 void GoodStep(Scalar /*quality*/ = 0) override {
76 if (update_available_) {
77 const Scalar curvature = update_step_.dot(update_gradient_delta_);
78 const Scalar minimum_curvature = this->options_.bfgs.curvature_threshold *
79 update_step_.norm() * update_gradient_delta_.norm();
80 if (std::isfinite(curvature) && curvature > minimum_curvature) {
81 const Scalar rho = Scalar(1) / curvature;
82 Matrix_t transform = Matrix_t::Identity(inverse_hessian_.rows(), inverse_hessian_.cols());
83 transform.noalias() -= rho * update_step_ * update_gradient_delta_.transpose();
84 Matrix_t updated = (transform * inverse_hessian_ * transform.transpose()).eval();
85 updated.noalias() += rho * update_step_ * update_step_.transpose();
86 inverse_hessian_.swap(updated);
87 }
88 }
89 update_available_ = false;
90 step_size_ = std::min<Scalar>(this->options_.bfgs.max_step_size,
91 step_size_ * this->options_.bfgs.step_growth);
92 }
93
94 void BadStep(Scalar /*quality*/ = 0) override {
95 step_size_ *= this->options_.bfgs.step_reduction;
96 has_gradient_ = false;
97 has_step_ = false;
98 update_available_ = false;
99 }
100
101 private:
102 void InitializeWorkspace() {
103 const Index dims = this->grad_.size();
104 if (!matrix_initialized_ || inverse_hessian_.rows() != dims) {
105 inverse_hessian_.resize(dims, dims);
106 inverse_hessian_.setIdentity();
107 update_previous_gradient_.resize(dims);
108 update_step_.resize(dims);
109 update_gradient_delta_.resize(dims);
110 last_step_.resize(dims);
111 matrix_initialized_ = true;
112 has_gradient_ = false;
113 has_step_ = false;
114 }
115 }
116
117 mutable Matrix_t inverse_hessian_;
118 Vector<Scalar, Dims> update_previous_gradient_;
119 Vector<Scalar, Dims> update_step_;
120 Vector<Scalar, Dims> update_gradient_delta_;
121 mutable Vector<Scalar, Dims> last_step_;
122 Scalar step_size_;
123 bool matrix_initialized_ = false;
124 bool has_gradient_ = false;
125 mutable bool has_step_ = false;
126 bool update_available_ = false;
127};
128
129template <typename Gradient_t = VecX, std::size_t HistoryCapacity = 8>
130class SolverLBFGS : public SolverGD<Gradient_t> {
131 public:
132 using Base = SolverGD<Gradient_t>;
133 using Scalar = typename Base::Scalar;
134 using Grad_t = typename Base::Grad_t;
135 static constexpr Index Dims = Base::Dims;
136 static constexpr bool IsNLLS = false;
137 static constexpr bool FirstOrder = true;
138
139 explicit SolverLBFGS(const Options &options = {})
140 : Base(options),
141 step_size_(options.lbfgs.step_size),
142 history_limit_(std::clamp<std::size_t>(options.lbfgs.history_size, 1, HistoryCapacity)) {}
143
144 void reset() {
145 Base::reset();
146 step_size_ = this->options_.lbfgs.step_size;
147 history_count_ = 0;
148 history_start_ = 0;
149 has_gradient_ = false;
150 has_step_ = false;
151 update_available_ = false;
152 }
153
154 template <typename X_t, typename AccFunc>
155 bool Build(const X_t &x, const AccFunc &acc, bool resize_and_clear = true) {
156 if constexpr (Dims == Dynamic) this->ResizeIfNeeded(x);
157 ResizeWorkspace();
158
159 const bool can_update = has_gradient_ && has_step_;
160 if (can_update) {
161 update_step_ = last_step_;
162 update_previous_gradient_ = this->grad_;
163 }
164
165 if (!Base::Build(x, acc, resize_and_clear)) {
166 has_gradient_ = false;
167 update_available_ = false;
168 return false;
169 }
170
171 update_available_ = can_update;
172 if (can_update) update_gradient_delta_ = this->grad_ - update_previous_gradient_;
173 has_gradient_ = true;
174 has_step_ = false;
175 return true;
176 }
177
178 std::optional<Vector<Scalar, Dims>> Solve() const override {
179 if (!this->cost().isValid()) return std::nullopt;
180
181 Vector<Scalar, Dims> q = this->grad_;
182 for (std::size_t offset = 0; offset < history_count_; ++offset) {
183 const std::size_t index = HistoryIndex(history_count_ - offset - 1);
184 alpha_[index] = rho_[index] * step_history_[index].dot(q);
185 q.noalias() -= alpha_[index] * gradient_history_[index];
186 }
187
188 Scalar scale = 1;
189 if (history_count_ > 0) {
190 const std::size_t newest = HistoryIndex(history_count_ - 1);
191 const Scalar yy = gradient_history_[newest].squaredNorm();
192 if (yy > std::numeric_limits<Scalar>::epsilon())
193 scale = step_history_[newest].dot(gradient_history_[newest]) / yy;
194 }
195 Vector<Scalar, Dims> direction = -scale * q;
196
197 for (std::size_t offset = 0; offset < history_count_; ++offset) {
198 const std::size_t index = HistoryIndex(offset);
199 const Scalar beta = rho_[index] * gradient_history_[index].dot(direction);
200 direction.noalias() += (alpha_[index] - beta) * step_history_[index];
201 }
202
203 if (!direction.allFinite() || this->grad_.dot(direction) >= 0) {
204 history_count_ = 0;
205 history_start_ = 0;
206 direction = -this->grad_;
207 }
208 last_step_ = step_size_ * direction;
209 has_step_ = true;
210 return last_step_;
211 }
212
213 void GoodStep(Scalar /*quality*/ = 0) override {
214 if (update_available_) {
215 const Scalar curvature = update_step_.dot(update_gradient_delta_);
216 const Scalar minimum_curvature = this->options_.lbfgs.curvature_threshold *
217 update_step_.norm() * update_gradient_delta_.norm();
218 if (std::isfinite(curvature) && curvature > minimum_curvature) StorePair(curvature);
219 }
220 update_available_ = false;
221 step_size_ = std::min<Scalar>(this->options_.lbfgs.max_step_size,
222 step_size_ * this->options_.lbfgs.step_growth);
223 }
224
225 void BadStep(Scalar /*quality*/ = 0) override {
226 step_size_ *= this->options_.lbfgs.step_reduction;
227 has_gradient_ = false;
228 has_step_ = false;
229 update_available_ = false;
230 }
231
232 private:
233 std::size_t HistoryIndex(std::size_t offset) const {
234 return (history_start_ + offset) % HistoryCapacity;
235 }
236
237 void ResizeWorkspace() {
238 const Index dims = this->grad_.size();
239 if (workspace_dims_ == dims) return;
240 for (auto &step : step_history_) step.resize(dims);
241 for (auto &gradient : gradient_history_) gradient.resize(dims);
242 update_previous_gradient_.resize(dims);
243 update_step_.resize(dims);
244 update_gradient_delta_.resize(dims);
245 last_step_.resize(dims);
246 workspace_dims_ = dims;
247 history_count_ = 0;
248 history_start_ = 0;
249 has_gradient_ = false;
250 has_step_ = false;
251 }
252
253 void StorePair(Scalar curvature) {
254 std::size_t index;
255 if (history_count_ < history_limit_) {
256 index = HistoryIndex(history_count_);
257 ++history_count_;
258 } else {
259 index = history_start_;
260 history_start_ = (history_start_ + 1) % HistoryCapacity;
261 }
262 step_history_[index] = update_step_;
263 gradient_history_[index] = update_gradient_delta_;
264 rho_[index] = Scalar(1) / curvature;
265 }
266
267 std::array<Vector<Scalar, Dims>, HistoryCapacity> step_history_;
268 std::array<Vector<Scalar, Dims>, HistoryCapacity> gradient_history_;
269 mutable std::array<Scalar, HistoryCapacity> alpha_{};
270 std::array<Scalar, HistoryCapacity> rho_{};
271 Vector<Scalar, Dims> update_previous_gradient_;
272 Vector<Scalar, Dims> update_step_;
273 Vector<Scalar, Dims> update_gradient_delta_;
274 mutable Vector<Scalar, Dims> last_step_;
275 Scalar step_size_;
276 std::size_t history_limit_;
277 mutable std::size_t history_count_ = 0;
278 mutable std::size_t history_start_ = 0;
279 Index workspace_dims_ = 0;
280 bool has_gradient_ = false;
281 mutable bool has_step_ = false;
282 bool update_available_ = false;
283};
284
285} // namespace tinyopt::solvers
std::optional< Vector< Scalar, Dims > > Solve() const override
Solve the linear system dx = -H^-1 * grad, returns nullopt on failure.
Definition bfgs.h:61
bool Build(const X_t &x, const AccFunc &acc, bool resize_and_clear=true)
Definition gd.h:116
bool ResizeIfNeeded(const X_t &x)
Definition gd.h:68
std::optional< Vector< Scalar, Dims > > Solve() const override
Solve the linear system dx = -lr * grad, returns nullopt on failure.
Definition bfgs.h:178
Definition options.h:18