Sleipnir C++ API
Loading...
Searching...
No Matches
newton.hpp
1// Copyright (c) Sleipnir contributors
2
3#pragma once
4
5#include <chrono>
6#include <cmath>
7#include <functional>
8#include <limits>
9#include <span>
10
11#include <Eigen/Core>
12#include <Eigen/SparseCore>
13#include <gch/small_vector.hpp>
14
15#include "sleipnir/optimization/solver/exit_status.hpp"
16#include "sleipnir/optimization/solver/iteration_info.hpp"
17#include "sleipnir/optimization/solver/newton_matrix_callbacks.hpp"
18#include "sleipnir/optimization/solver/options.hpp"
19#include "sleipnir/optimization/solver/util/error_estimate.hpp"
20#include "sleipnir/optimization/solver/util/filter.hpp"
21#include "sleipnir/optimization/solver/util/kkt_error.hpp"
22#include "sleipnir/optimization/solver/util/regularized_ldlt.hpp"
23#include "sleipnir/util/assert.hpp"
24#include "sleipnir/util/print_diagnostics.hpp"
25#include "sleipnir/util/scope_exit.hpp"
26#include "sleipnir/util/scoped_profiler.hpp"
27#include "sleipnir/util/solve_profiler.hpp"
28#include "sleipnir/util/symbol_exports.hpp"
29
30// See docs/algorithms.md#Works_cited for citation definitions.
31
32namespace slp {
33
54template <typename Scalar>
55ExitStatus newton(
56 const NewtonMatrixCallbacks<Scalar>& matrix_callbacks,
57 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
58 iteration_callbacks,
59 const Options& options, Eigen::Vector<Scalar, Eigen::Dynamic>& x) {
60 using std::isfinite;
61
62 const auto solve_start_time = std::chrono::steady_clock::now();
63
64 gch::small_vector<SolveProfiler> solve_profilers;
65 solve_profilers.emplace_back("solver");
66 solve_profilers.emplace_back(" ↳ setup");
67 solve_profilers.emplace_back(" ↳ iteration");
68 solve_profilers.emplace_back(" ↳ feasibility ✓");
69 solve_profilers.emplace_back(" ↳ iter callbacks");
70 solve_profilers.emplace_back(" ↳ KKT matrix decomp");
71 solve_profilers.emplace_back(" ↳ KKT system solve");
72 solve_profilers.emplace_back(" ↳ line search");
73 solve_profilers.emplace_back(" ↳ next iter prep");
74 solve_profilers.emplace_back(" ↳ f(x)");
75 solve_profilers.emplace_back(" ↳ ∇f(x)");
76 solve_profilers.emplace_back(" ↳ ∇²ₓₓL");
77
78 auto& solver_prof = solve_profilers[0];
79 auto& setup_prof = solve_profilers[1];
80 auto& inner_iter_prof = solve_profilers[2];
81 auto& feasibility_check_prof = solve_profilers[3];
82 auto& iter_callbacks_prof = solve_profilers[4];
83 auto& kkt_matrix_decomp_prof = solve_profilers[5];
84 auto& kkt_system_solve_prof = solve_profilers[6];
85 auto& line_search_prof = solve_profilers[7];
86 auto& next_iter_prep_prof = solve_profilers[8];
87
88 // Set up profiled matrix callbacks
89#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
90 auto& f_prof = solve_profilers[9];
91 auto& g_prof = solve_profilers[10];
92 auto& H_prof = solve_profilers[11];
93
94 NewtonMatrixCallbacks<Scalar> matrices{
95 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x) -> Scalar {
96 ScopedProfiler prof{f_prof};
97 return matrix_callbacks.f(x);
98 },
99 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
100 -> Eigen::SparseVector<Scalar> {
101 ScopedProfiler prof{g_prof};
102 return matrix_callbacks.g(x);
103 },
104 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
105 -> Eigen::SparseMatrix<Scalar> {
106 ScopedProfiler prof{H_prof};
107 return matrix_callbacks.H(x);
108 }};
109#else
110 const auto& matrices = matrix_callbacks;
111#endif
112
113 solver_prof.start();
114 setup_prof.start();
115
116 Scalar f = matrices.f(x);
117
118 int num_decision_variables = x.rows();
119
120 Eigen::SparseVector<Scalar> g = matrices.g(x);
121 Eigen::SparseMatrix<Scalar> H = matrices.H(x);
122
123 // Ensure matrix callback dimensions are consistent
124 slp_assert(g.rows() == num_decision_variables);
125 slp_assert(H.rows() == num_decision_variables);
126 slp_assert(H.cols() == num_decision_variables);
127
128 // Check whether initial guess has finite f(xₖ)
129 if (!isfinite(f)) {
130 return ExitStatus::NONFINITE_INITIAL_COST_OR_CONSTRAINTS;
131 }
132
133 int iterations = 0;
134
135 Filter<Scalar> filter;
136
137 RegularizedLDLT<Scalar> solver{num_decision_variables, 0};
138
139 // Variables for determining when a step is acceptable
140 constexpr Scalar α_reduction_factor(0.5);
141 constexpr Scalar α_min(1e-20);
142
143 // Error estimate
144 Scalar E_0 = std::numeric_limits<Scalar>::infinity();
145
146 setup_prof.stop();
147
148 // Prints final solver diagnostics when the solver exits
149 scope_exit exit{[&] {
150 if (options.diagnostics) {
151 solver_prof.stop();
152 if (iterations > 0) {
153 print_bottom_iteration_diagnostics();
154 }
155 print_solver_diagnostics(solve_profilers);
156 }
157 }};
158
159 while (E_0 > Scalar(options.tolerance)) {
160 ScopedProfiler inner_iter_profiler{inner_iter_prof};
161 ScopedProfiler feasibility_check_profiler{feasibility_check_prof};
162
163 // Check for diverging iterates
164 if (x.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !x.allFinite()) {
165 return ExitStatus::DIVERGING_ITERATES;
166 }
167
168 feasibility_check_profiler.stop();
169 ScopedProfiler iter_callbacks_profiler{iter_callbacks_prof};
170
171 // Call iteration callbacks
172 for (const auto& callback : iteration_callbacks) {
173 if (callback({iterations, x, g, H, Eigen::SparseMatrix<Scalar>{},
174 Eigen::SparseMatrix<Scalar>{}})) {
175 return ExitStatus::CALLBACK_REQUESTED_STOP;
176 }
177 }
178
179 iter_callbacks_profiler.stop();
180 ScopedProfiler kkt_matrix_decomp_profiler{kkt_matrix_decomp_prof};
181
182 // Solve the Newton-KKT system
183 //
184 // Hpˣ = −∇f
185 solver.compute(H);
186
187 kkt_matrix_decomp_profiler.stop();
188 ScopedProfiler kkt_system_solve_profiler{kkt_system_solve_prof};
189
190 Eigen::Vector<Scalar, Eigen::Dynamic> p_x = solver.solve(-g);
191
192 kkt_system_solve_profiler.stop();
193 ScopedProfiler line_search_profiler{line_search_prof};
194
195 constexpr Scalar α_max(1);
196 Scalar α = α_max;
197
198 // Loop until a step is accepted. If a step becomes acceptable, the loop
199 // will exit early.
200 while (1) {
201 Eigen::Vector<Scalar, Eigen::Dynamic> trial_x = x + α * p_x;
202
203 Scalar trial_f = matrices.f(trial_x);
204
205 // If f(xₖ + αpₖˣ) isn't finite, reduce step size immediately
206 if (!isfinite(trial_f)) {
207 // Reduce step size
208 α *= α_reduction_factor;
209
210 if (α < α_min) {
211 return ExitStatus::LINE_SEARCH_FAILED;
212 }
213 continue;
214 }
215
216 // Check whether filter accepts trial iterate
217 if (filter.try_add(FilterEntry{trial_f}, α)) {
218 // Accept step
219 break;
220 }
221
222 // Reduce step size
223 α *= α_reduction_factor;
224
225 // If step size hit a minimum, check if the KKT error was reduced. If it
226 // wasn't, report bad line search.
227 if (α < α_min) {
228 Scalar current_kkt_error = kkt_error<Scalar>(g);
229
230 Eigen::Vector<Scalar, Eigen::Dynamic> trial_x = x + α_max * p_x;
231
232 Scalar next_kkt_error = kkt_error<Scalar>(matrices.g(trial_x));
233
234 // If the step using αᵐᵃˣ reduced the KKT error, accept it anyway
235 if (next_kkt_error <= Scalar(0.999) * current_kkt_error) {
236 α = α_max;
237
238 // Accept step
239 break;
240 }
241
242 return ExitStatus::LINE_SEARCH_FAILED;
243 }
244 }
245
246 line_search_profiler.stop();
247
248 // xₖ₊₁ = xₖ + αₖpₖˣ
249 x += α * p_x;
250
251 // Update autodiff for Hessian
252 f = matrices.f(x);
253 g = matrices.g(x);
254 H = matrices.H(x);
255
256 ScopedProfiler next_iter_prep_profiler{next_iter_prep_prof};
257
258 // Update the error estimate
259 E_0 = error_estimate<Scalar>(g);
260
261 next_iter_prep_profiler.stop();
262 inner_iter_profiler.stop();
263
264 if (options.diagnostics) {
265 print_iteration_diagnostics(iterations, IterationType::NORMAL,
266 inner_iter_profiler.current_duration(), E_0,
267 f, Scalar(0), Scalar(0), Scalar(0),
268 solver.hessian_regularization(), α, α_max,
269 α_reduction_factor, Scalar(1));
270 }
271
272 ++iterations;
273
274 // Check for max iterations
275 if (iterations >= options.max_iterations) {
276 return ExitStatus::MAX_ITERATIONS_EXCEEDED;
277 }
278
279 // Check for max wall clock time
280 if (std::chrono::steady_clock::now() - solve_start_time > options.timeout) {
281 return ExitStatus::TIMEOUT;
282 }
283 }
284
285 return ExitStatus::SUCCESS;
286}
287
288extern template SLEIPNIR_DLLEXPORT ExitStatus
289newton(const NewtonMatrixCallbacks<double>& matrix_callbacks,
290 std::span<std::function<bool(const IterationInfo<double>& info)>>
291 iteration_callbacks,
292 const Options& options, Eigen::Vector<double, Eigen::Dynamic>& x);
293
294} // namespace slp