Sleipnir C++ API
Loading...
Searching...
No Matches
newton.hpp
1// Copyright (c) Sleipnir contributors
2
3#pragma once
4
5#include <algorithm>
6#include <chrono>
7#include <cmath>
8#include <functional>
9#include <limits>
10#include <optional>
11#include <span>
12
13#include <Eigen/Core>
14#include <Eigen/SparseCore>
15
16#include "sleipnir/optimization/solver/exit_status.hpp"
17#include "sleipnir/optimization/solver/iteration_info.hpp"
18#include "sleipnir/optimization/solver/newton_matrix_callbacks.hpp"
19#include "sleipnir/optimization/solver/options.hpp"
20#include "sleipnir/optimization/solver/util/all_finite.hpp"
21#include "sleipnir/optimization/solver/util/filter.hpp"
22#include "sleipnir/optimization/solver/util/kkt_error.hpp"
23#include "sleipnir/optimization/solver/util/kkt_solver.hpp"
24#include "sleipnir/optimization/solver/util/time_trace.hpp"
25#include "sleipnir/util/assert.hpp"
26#include "sleipnir/util/print_diagnostics.hpp"
27#include "sleipnir/util/symbol_exports.hpp"
28#include "sleipnir/util/time_tracer.hpp"
29
30// See docs/algorithms.md#Works_cited for citation definitions.
31
32namespace slp {
33
52template <typename Scalar>
53ExitStatus newton(
54 const NewtonMatrixCallbacks<Scalar>& matrix_callbacks,
55 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
56 iteration_callbacks,
57 const Options<Scalar>& options, Eigen::VectorX<Scalar>& x) {
58 using DenseVector = Eigen::VectorX<Scalar>;
59 using SparseMatrix = Eigen::SparseMatrix<Scalar>;
60 using SparseVector = Eigen::SparseVector<Scalar>;
61
62 using std::isfinite;
63
64 const auto solve_start_time = std::chrono::steady_clock::now();
65
66 using enum SolveTimeTracerID;
67
68 int iterations = 0;
69
70 // Prints final solver diagnostics when the solver exits
71 SolveDiagnostics<newton_time_trace> diagnostics{options.diagnostics,
72 options.diagnostics};
73
74 // Set up traced matrix callbacks if diagnostics are enabled
75#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
76 std::optional<NewtonMatrixCallbacks<Scalar>> traced_matrices;
77 if (options.diagnostics) {
78 traced_matrices.emplace(matrix_callbacks.num_decision_variables,
79 traced(diagnostics[COST], matrix_callbacks.f),
80 traced(diagnostics[GRADIENT], matrix_callbacks.g),
81 traced(diagnostics[HESSIAN], matrix_callbacks.H),
82 matrix_callbacks.scaling);
83 }
84 const auto& matrices = traced_matrices ? *traced_matrices : matrix_callbacks;
85#else
86 const auto& matrices = matrix_callbacks;
87#endif
88
89 ScopedTimeTracer setup_tracer{diagnostics[SETUP]};
90
91 Scalar f = matrices.f(x);
92 SparseVector g = matrices.g(x);
93 SparseMatrix H = matrices.H(x);
94
95 // Ensure matrix callback dimensions are consistent
96 slp_assert(g.rows() == matrices.num_decision_variables);
97 slp_assert(H.rows() == matrices.num_decision_variables);
98 slp_assert(H.cols() == matrices.num_decision_variables);
99
100 DenseVector trial_x;
101
102 Scalar trial_f;
103
104 // Check whether initial guess has finite cost and derivatives
105 if (!isfinite(f) || !all_finite(g) || !all_finite(H)) {
106 return ExitStatus::NONFINITE_INITIAL_GUESS;
107 }
108
109 Filter<Scalar> filter;
110
111 KKTSolver<Scalar> solver{
112 // Use sparse solver if lower triangle fills < 25% of system
113 H.nonZeros() < 0.25 * H.size(), matrices.num_decision_variables, 0};
114
115 // Step size reduction factor
116 constexpr Scalar α_reduction_factor(0.5);
117
118 // The smallest KKT error of a step accepted by the KKT error reduction
119 // fallback. Each fallback step must improve on it, so finitely many fallback
120 // steps are taken before the solver converges.
121 Scalar fallback_kkt_error = std::numeric_limits<Scalar>::infinity();
122
123 // Error
124 Scalar E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
125 matrices.scaling, g);
126
127 setup_tracer.stop();
128
129 // Prints initial iterate diagnostics now and closes the iterate diagnostics
130 // table when the solver exits
131 IterateDiagnosticsTable table{options.diagnostics, E_0, f,
132 Scalar(0), Scalar(0), Scalar(0)};
133
134 while (E_0 > options.tolerance) {
135 ScopedTimeTracer inner_iter_tracer{diagnostics[ITERATION]};
136
137 // Check for diverging iterates
138 if (x.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !x.allFinite()) {
139 return ExitStatus::DIVERGING_ITERATES;
140 }
141
142 PhaseTimeTracer phase_tracer{diagnostics[CALLBACKS]};
143
144 // Call iteration callbacks
145 for (const auto& callback : iteration_callbacks) {
146 if (callback({iterations, x, {}, {}, {}, g, H, {}, {}})) {
147 return ExitStatus::CALLBACK_REQUESTED_STOP;
148 }
149 }
150
151 phase_tracer.next(diagnostics[KKT_MATRIX_DECOMP]);
152
153 // Solve the Newton-KKT system
154 //
155 // Hpˣ = −∇f
156 solver.compute(H);
157
158 phase_tracer.next(diagnostics[KKT_SYSTEM_SOLVE]);
159
160 DenseVector p_x = solver.solve(-g);
161
162 phase_tracer.next(diagnostics[LINE_SEARCH]);
163
164 constexpr Scalar α_max(1);
165 Scalar α = α_max;
166 const FilterEntry<Scalar> current_entry{f};
167 const Scalar D_ϕ = g.transpose() * p_x;
168
169 const Scalar α_min = filter.min_step_size(current_entry, D_ϕ);
170
171 // α_min is zero when the search direction is a descent direction, so also
172 // end the line search when the step becomes negligible relative to the
173 // iterate. This bounds the number of step size reductions.
174 //
175 // A step αpˣ is negligible if
176 //
177 // ‖αpˣ/(1 + |x|)‖_∞ < 10ε
178 //
179 // where the absolute value and division are elementwise. Since α > 0, it
180 // factors out of the norm.
181 //
182 // α‖pˣ/(1 + |x|)‖_∞ < 10ε
183 //
184 // Dividing both sides by the norm gives
185 //
186 // α < 10ε / ‖pˣ/(1 + |x|)‖_∞
187 const Scalar α_tiny = Scalar(10) * std::numeric_limits<Scalar>::epsilon() /
188 (p_x.array().abs() / (Scalar(1) + x.array().abs()))
189 .matrix()
190 .template lpNorm<Eigen::Infinity>();
191
192 // Loop until a step is accepted
193 while (1) {
194 trial_x = x + α * p_x;
195
196 trial_f = matrices.f(trial_x);
197
198 // If f(xₖ + αpₖˣ) isn't finite, reduce step size immediately
199 if (!isfinite(trial_f)) {
200 // Reduce step size
201 α *= α_reduction_factor;
202
203 if (α < α_min || α < α_tiny) {
204 return ExitStatus::LINE_SEARCH_FAILED;
205 }
206 continue;
207 }
208
209 // Check whether filter accepts trial iterate
210 if (filter.try_add(current_entry, FilterEntry{trial_f}, D_ϕ, α)) {
211 // Accept step
212 break;
213 }
214
215 // Reduce step size
216 α *= α_reduction_factor;
217
218 // If step size hit a minimum or the step became negligible, check if the
219 // KKT error was reduced. If it wasn't, report line search failure.
220 if (α < α_min || α < α_tiny) {
221 Scalar current_kkt_error = kkt_error<Scalar, KKTErrorType::ONE_NORM>(g);
222
223 trial_x = x + α_max * p_x;
224
225 trial_f = matrices.f(trial_x);
226
227 Scalar next_kkt_error =
228 kkt_error<Scalar, KKTErrorType::ONE_NORM>(matrices.g(trial_x));
229
230 // If the step using αᵐᵃˣ reduced the KKT error below the current
231 // iterate's and every previous fallback step's, accept it anyway
232 if (next_kkt_error <=
233 Scalar(0.999) * std::min(current_kkt_error, fallback_kkt_error)) {
234 // Accept step
235 fallback_kkt_error = next_kkt_error;
236 α = α_max;
237 break;
238 }
239
240 return ExitStatus::LINE_SEARCH_FAILED;
241 }
242 }
243
244 phase_tracer.stop();
245
246 // Update iterates
247 x = trial_x;
248
249 f = trial_f;
250
251 // Update autodiff for Hessian
252 g = matrices.g(x);
253 H = matrices.H(x);
254
255 // Update the error
256 E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
257 matrices.scaling, g);
258
259 inner_iter_tracer.stop();
260
261 if (options.diagnostics) {
262 print_iterate_diagnostics(
263 iterations, IterationType::NORMAL,
264 inner_iter_tracer.current_duration(), E_0, f, Scalar(0), Scalar(0),
265 Scalar(0), solver.hessian_regularization(),
266 solver.constraint_jacobian_regularization(),
267 p_x.template lpNorm<Eigen::Infinity>(), Scalar(1), α, α_max,
268 α_reduction_factor, Scalar(1));
269 }
270
271 ++iterations;
272
273 // Check for max iterations
274 if (iterations >= options.max_iterations) {
275 return ExitStatus::MAX_ITERATIONS_EXCEEDED;
276 }
277
278 // Check for max wall clock time
279 if (std::chrono::steady_clock::now() - solve_start_time > options.timeout) {
280 return ExitStatus::TIMEOUT;
281 }
282 }
283
284 if (!isfinite(E_0)) {
285 return ExitStatus::DIVERGING_ITERATES;
286 } else {
287 return ExitStatus::SUCCESS;
288 }
289}
290
291extern template SLEIPNIR_DLLEXPORT ExitStatus
292newton(const NewtonMatrixCallbacks<double>& matrix_callbacks,
293 std::span<std::function<bool(const IterationInfo<double>& info)>>
294 iteration_callbacks,
295 const Options<double>& options, Eigen::VectorX<double>& x);
296
297} // namespace slp