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