alpaqa develop
Nonconvex constrained optimization
Loading...
Searching...
No Matches
gradient-checker.cpp
Go to the documentation of this file.
1#ifdef _WIN32
2#define NOMINMAX
3#include <Windows.h>
4#endif
5
10#include <alpaqa-version.h>
11
12#include "options.hpp"
13#include "problem.hpp"
14
15#include <Eigen/Sparse>
16
17#ifdef ALPAQA_WITH_EXTERNAL_CASADI
18#include <casadi/config.h>
19#endif
20#ifdef ALPAQA_WITH_IPOPT
21#include <IpoptConfig.h>
22#endif
23
24#include <algorithm>
25#include <filesystem>
26#include <fstream>
27#include <iostream>
28#include <random>
29#include <span>
30#include <stdexcept>
31#include <string>
32#include <string_view>
33#include <tuple>
34#include <type_traits>
35namespace fs = std::filesystem;
36
38
39const auto *docs = R"==(
40problem types:
41 dl: Dynamically loaded problem using the DLProblem class.
42 Specify the name of the registration function using the
43 problem.register option, e.g. problem.register=register_alpaqa_problem.
44 Further options can be passed to the problem using
45 problem.<key>[=<value>].
46 cs: Load a CasADi problem using the CasADiProblem class.
47 If a .tsv file with the same name as the shared library file exists,
48 the bounds and parameters will be loaded from that file. See
49 CasADiProblem::load_numerical_data for more details.
50 The problem parameter can be set using the problem.param option.
51 cu: Load a CUTEst problem using the CUTEstProblem class.
52
53options:
54 --full-print
55 Print the full gradients.
56 --no-hessians
57 Do not check any Hessian matrices.
58 --seed=<seed>
59 Seed for the random number generator.
60)==";
61
62void print_usage(const char *a0) {
63 const auto *opts = " [<problem-type>:][<path>/]<name> [options...]\n";
64 std::cout << "alpaqa gradient-checker (" ALPAQA_VERSION_FULL ")\n\n"
65 " Command-line utility to check problem gradients.\n"
66 " alpaqa is published under the LGPL-3.0.\n"
67 " https://github.com/kul-optec/alpaqa"
68 "\n\n"
69 " Usage: "
70 << a0 << opts << docs << std::endl;
71 std::cout << "Third-party libraries:\n"
72 << " * Eigen " << EIGEN_WORLD_VERSION << '.'
73 << EIGEN_MAJOR_VERSION << '.' << EIGEN_MINOR_VERSION
74 << " (https://gitlab.com/libeigen/eigen) - MPL-2.0\n"
75#ifdef ALPAQA_WITH_EXTERNAL_CASADI
76 << " * CasADi " CASADI_VERSION_STRING
77 " (https://github.com/casadi/casadi) - LGPL-3.0-or-later\n"
78#endif
79#ifdef ALPAQA_WITH_CUTEST
80 << " * CUTEst"
81 " (https://github.com/ralna/CUTEst) - BSD-3-Clause\n"
82#endif
83 << std::endl;
84}
85
86/// Split the string @p s on the first occurrence of @p tok.
87/// Returns ("", s) if tok was not found.
88auto split_once(std::string_view s, char tok = '.') {
89 auto tok_pos = s.find(tok);
90 if (tok_pos == s.npos)
91 return std::make_tuple(std::string_view{}, s);
92 std::string_view key{s.begin(), s.begin() + tok_pos};
93 std::string_view rem{s.begin() + tok_pos + 1, s.end()};
94 return std::make_tuple(key, rem);
95}
96
97auto get_problem_path(const char *const *argv) {
98 bool rel_to_exe = argv[1][0] == '^';
99 std::string_view prob_path_s = argv[1] + static_cast<ptrdiff_t>(rel_to_exe);
100 std::string_view prob_type;
101 std::tie(prob_type, prob_path_s) = split_once(prob_path_s, ':');
102 fs::path prob_path{prob_path_s};
103 if (rel_to_exe)
104 prob_path = fs::canonical(fs::path(argv[0])).parent_path() / prob_path;
105 return std::make_tuple(std::move(prob_path), prob_type);
106}
107
113
114void check_gradients(LoadedProblem &, std::ostream &,
115 const CheckGradientsOpts &);
116
117int main(int argc, const char *argv[]) try {
118#ifdef _WIN32
119 SetConsoleOutputCP(CP_UTF8);
120#endif
121 // Check command line options
122 if (argc < 1)
123 return -1;
124 if (argc == 1)
125 return print_usage(argv[0]), 0;
126 if (argc < 2)
127 return print_usage(argv[0]), -1;
128 std::span args{argv, static_cast<size_t>(argc)};
129 Options opts{argc - 2, argv + 2};
130
131 // Check where to write the output to
132 std::ostream &os = std::cout;
133
134 // Check which problem to load
135 auto [prob_path, prob_type] = get_problem_path(argv);
136
137 // Load problem
138 os << "Loading problem " << prob_path << std::endl;
139 auto problem = load_problem(prob_type, prob_path.parent_path(),
140 prob_path.filename(), opts);
141 os << "Loaded problem " << problem.path.stem().string() << " from "
142 << problem.path << "\nnvar: " << problem.problem.get_n()
143 << "\nncon: " << problem.problem.get_m() << "\nProvided functions:\n";
144 alpaqa::print_provided_functions(os, problem.problem);
145 os << std::endl;
146
147 // Options
148 auto has_opt = [&opts](std::string_view o) {
149 auto o_it = std::ranges::find(opts.options(), o);
150 if (o_it == opts.options().end())
151 return false;
152 auto index = static_cast<size_t>(o_it - opts.options().begin());
153 ++opts.used()[index];
154 return true;
155 };
156 CheckGradientsOpts cg_opts{
157 .print_full = has_opt("--full-print"),
158 .hessians = !has_opt("--no-hessians"),
159 .scale_perturbations = 1e-2,
160 };
161 set_params(cg_opts.scale_perturbations, "--scale-perturbations", opts);
162
163 // Seed rand
164 auto seed = static_cast<unsigned int>(std::time(nullptr));
165 set_params(seed, "--seed", opts);
166 std::srand(seed);
167
168 // Check options
169 auto used = opts.used();
170 auto unused_opt = std::ranges::find(used, 0);
171 auto unused_idx = static_cast<size_t>(unused_opt - used.begin());
172 if (unused_opt != used.end())
173 throw std::invalid_argument("Unused option: " +
174 std::string(opts.options()[unused_idx]));
175
176 // Check gradients
177 check_gradients(problem, os, cg_opts);
178
179} catch (std::exception &e) {
180 std::cerr << "Error: " << demangled_typename(typeid(e)) << ":\n "
181 << e.what() << std::endl;
182 return -1;
183}
184
186 crvec x, crvec y, rvec grad_L, rvec work_n) {
187 if (y.size() == 0) /* [[unlikely]] */
188 return problem.eval_grad_f(x, grad_L);
189 problem.eval_grad_f_grad_g_prod(x, y, grad_L, work_n);
190 grad_L += work_n;
191}
192
194 crvec x, crvec y, crvec Σ, rvec grad_ψ, rvec work_n,
195 rvec work_m) -> real_t {
196 if (y.size() == 0) /* [[unlikely]] */
197 return problem.eval_f_grad_f(x, grad_ψ);
198
199 auto &ŷ = work_m;
200 // ψ(x) = f(x) + ½ dᵀŷ
201 auto f = problem.eval_f_g(x, ŷ);
202 auto dᵀŷ = problem.calc_ŷ_dᵀŷ(ŷ, y, Σ);
203 auto ψ = f + real_t(0.5) * dᵀŷ;
204 // ∇ψ(x) = ∇f(x) + ∇g(x) ŷ
205 try {
206 default_eval_grad_L(problem, x, ŷ, grad_ψ, work_n);
208 problem.eval_grad_L(x, ŷ, grad_ψ, work_n);
209 }
210 return ψ;
211}
212
213auto finite_diff(const std::function<real_t(crvec)> &f, crvec x) {
214 const auto n = x.size();
215 vec grad(n);
216 vec h = vec::Zero(n);
217 const auto ε = 5e-6;
218 const auto δ = 1e-2 * ε;
219 const real_t fx = f(x);
220 for (index_t i = 0; i < n; ++i) {
221 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
222 h(i) = hh;
223 grad.coeffRef(i) = (f(x + h) - fx) / hh;
224 h(i) = 0;
225 }
226 return std::make_tuple(fx, std::move(grad));
227}
228
229auto finite_diff_hess_sparse(const std::function<void(crvec, rvec)> &grad_L,
230 crvec x) {
231 const auto n = x.size();
232 std::vector<Eigen::Triplet<real_t, index_t>> coo;
233 vec h = vec::Zero(n);
234 const auto ε = 5e-6;
235 const auto δ = 1e-2 * ε;
236 vec grad_x(n), grad_xh(n);
237 grad_L(x, grad_x);
238 for (index_t i = 0; i < n; ++i) {
239 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
240 h(i) = hh;
241 grad_L(x + h, grad_xh);
242 grad_xh = (grad_xh - grad_x) / hh;
243 for (index_t j = 0; j < n; ++j)
244 if (real_t v = grad_xh(j); v != 0)
245 coo.emplace_back(std::min(j, i), std::max(i, j),
246 v * (i == j ? 1 : 0.5));
247 h(i) = 0;
248 }
249 Eigen::SparseMatrix<real_t, 0, index_t> hess(n, n);
250 hess.setFromTriplets(coo.begin(), coo.end());
251 return hess;
252}
253
254auto finite_diff_hess(const std::function<void(crvec, rvec)> &grad_L, crvec x) {
255 const auto n = x.size();
256 vec h = vec::Zero(n);
257 const auto ε = 5e-6;
258 const auto δ = 1e-2 * ε;
259 vec grad_x(n), grad_xh(n);
260 mat hess(n, n);
261 grad_L(x, grad_x);
262 for (index_t i = 0; i < n; ++i) {
263 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
264 h(i) = hh;
265 grad_L(x + h, grad_xh);
266 hess.col(i) = (grad_xh - grad_x) / hh;
267 h(i) = 0;
268 }
269 return hess;
270}
271
272void check_gradients(LoadedProblem &lproblem, std::ostream &log,
273 const CheckGradientsOpts &opts) {
274 auto &te_problem = lproblem.problem;
275
276 auto x0 = lproblem.initial_guess_x;
277 auto y0 = lproblem.initial_guess_y;
278 auto sc = opts.scale_perturbations + x0.norm();
279 auto n = te_problem.get_n(), m = te_problem.get_m();
280
281 std::srand(static_cast<unsigned int>(std::time(nullptr)));
282 vec Σ = 1.5 * vec::Random(m).array() + 2;
283 vec y = y0 + (opts.scale_perturbations + y0.norm()) * vec::Random(m);
284 vec x = x0 + sc * vec::Random(n);
285 vec v = 5e-6 * sc * vec::Random(n);
286 vec gx(m);
287 vec wn(n), wm(m);
288
289 auto print_compare = [&log, &opts](const auto &other, const auto &ref) {
290 auto abs_err =
291 (ref - other).reshaped().template lpNorm<Eigen::Infinity>();
292 auto rel_err =
293 abs_err / ref.reshaped().template lpNorm<Eigen::Infinity>();
294 log << " abs error = " << alpaqa::float_to_str(abs_err) << '\n';
295 log << " rel error = " << alpaqa::float_to_str(rel_err) << '\n';
296 if (opts.print_full) {
297 alpaqa::print_python(log << " (1) = ", other);
298 alpaqa::print_python(log << " (2) = ", ref) << std::endl;
299 }
300 };
301 auto print_compare_scal = [&log, &opts](const auto &fd, const auto &ad) {
302 auto abs_err = std::abs(fd - ad);
303 auto rel_err = abs_err / std::abs(fd);
304 log << " abs error = " << alpaqa::float_to_str(abs_err) << '\n';
305 log << " rel error = " << alpaqa::float_to_str(rel_err) << '\n';
306 if (opts.print_full) {
307 log << " (1) = " << alpaqa::float_to_str(fd) << '\n';
308 log << " (2) = " << alpaqa::float_to_str(ad) << '\n' << std::endl;
309 }
310 };
311
312 auto f = [&](crvec x) { return te_problem.eval_f(x); };
313 log << "Gradient verification: ∇f(x) (grad_f compared to finite "
314 "differences of f)\n";
315 auto [fx, fd_grad_f] = finite_diff(f, x);
316 vec grad_f(n);
317 te_problem.eval_grad_f(x, grad_f);
318 print_compare(grad_f, fd_grad_f);
319
320 if (te_problem.provides_eval_f_grad_f()) {
321 log << "Gradient verification: ∇f(x) (f_grad_f compared to grad_f)\n";
322 vec f_grad_f(n);
323 auto f2 = te_problem.eval_f_grad_f(x, f_grad_f);
324 print_compare(f_grad_f, grad_f);
325 log << "Function verification: f(x) (f_grad_f compared to f)\n";
326 print_compare_scal(f2, fx);
327 }
328
329 log << "Gradient verification: ∇L(x) (grad_L compared to finite "
330 "differences of f + yᵀg)\n";
331 auto L = [&](crvec x) {
332 te_problem.eval_g(x, gx);
333 return te_problem.eval_f(x) + gx.dot(y);
334 };
335 auto [Lx, fd_grad_L] = finite_diff(L, x);
336 vec grad_L(n);
337 te_problem.eval_grad_L(x, y, grad_L, wn);
338 print_compare(grad_L, fd_grad_L);
339
340 log << "Gradient verification: ∇ψ(x) (grad_ψ compared to finite "
341 "differences of ψ)\n";
342 auto ψ = [&](crvec x) { return te_problem.eval_ψ(x, y, Σ, wm); };
343 auto [ψx, fd_grad_ψ] = finite_diff(ψ, x);
344 vec grad_ψ(n);
345 te_problem.eval_grad_ψ(x, y, Σ, grad_ψ, wn, wm);
346 print_compare(grad_ψ, fd_grad_ψ);
347
348 log << "Gradient verification: ∇ψ(x) (grad_ψ compared to reference "
349 "implementation based on g, ∇f, ∇g)\n";
350 vec grad_ψ_default(n);
351 auto ψ_default =
352 default_eval_ψ_grad_ψ(te_problem, x, y, Σ, grad_ψ_default, wn, wm);
353 print_compare(grad_ψ, grad_ψ_default);
354 log << "Function verification: ψ(x) (ψ compared to reference "
355 "implementation based on f, g)\n";
356 print_compare_scal(ψx, ψ_default);
357
358 if (te_problem.provides_eval_ψ_grad_ψ()) {
359 log << "Gradient verification: ∇ψ(x) (grad_ψ compared to ψ_grad_ψ)\n";
360 vec ψ_grad_ψ(n);
361 real_t ψ2 = te_problem.eval_ψ_grad_ψ(x, y, Σ, ψ_grad_ψ, wn, wm);
362 print_compare(grad_ψ, ψ_grad_ψ);
363 log << "Function verification: ψ(x) (ψ compared to ψ_grad_ψ)\n";
364 print_compare_scal(ψx, ψ2);
365 }
366
367 if (te_problem.provides_eval_hess_L_prod()) {
368 log << "Hessian product verification: ∇²L(x) (hess_L_prod compared to "
369 "finite differences of grad_L)\n";
370 vec grad_Lv(n);
371 vec xv = x + v;
372 te_problem.eval_grad_L(xv, y, grad_Lv, wn);
373 vec fd_hess_Lv = grad_Lv - grad_L;
374 vec hess_Lv(n);
375 te_problem.eval_hess_L_prod(x, y, 1, v, hess_Lv);
376 print_compare(hess_Lv, fd_hess_Lv);
377 }
378
379 if (te_problem.provides_eval_hess_ψ_prod()) {
380 log << "Hessian product verification: ∇²ψ(x) (hess_ψ_prod compared to "
381 "finite differences of grad_ψ)\n";
382 vec grad_ψv(n);
383 vec xv = x + v;
384 te_problem.eval_grad_ψ(xv, y, Σ, grad_ψv, wn, wm);
385 vec fd_hess_ψv = grad_ψv - grad_ψ;
386 vec hess_ψv(n);
387 te_problem.eval_hess_ψ_prod(x, y, Σ, 1, v, hess_ψv);
388 print_compare(hess_ψv, fd_hess_ψv);
389 }
390
391 if (opts.hessians && te_problem.provides_eval_hess_L()) {
392 log << "Hessian verification: ∇²L(x) (hess_L compared to finite "
393 "differences of grad_L)\n";
394 namespace sp = alpaqa::sparsity;
395 auto sparsity = te_problem.get_hess_L_sparsity();
396 sp::SparsityConverter<sp::Sparsity<config_t>, sp::Dense<config_t>> cvt{
397 sparsity};
398 mat hess_L(n, n);
399 auto eval_h = [&](rvec v) { te_problem.eval_hess_L(x, y, 1., v); };
400 cvt.convert_values(eval_h, hess_L.reshaped());
401 mat fd_hess_L = finite_diff_hess(
402 [&](crvec x, rvec g) { te_problem.eval_grad_L(x, y, g, wn); }, x);
403 print_compare(hess_L, fd_hess_L);
404 }
405
406 if (opts.hessians && te_problem.provides_eval_hess_ψ()) {
407 log << "Hessian verification: ∇²ψ(x) (hess_ψ compared to finite "
408 "differences of grad_ψ)\\n";
409 namespace sp = alpaqa::sparsity;
410 auto sparsity = te_problem.get_hess_ψ_sparsity();
411 sp::SparsityConverter<sp::Sparsity<config_t>, sp::Dense<config_t>> cvt{
412 sparsity};
413 mat hess_ψ(n, n);
414 auto eval_h = [&](rvec v) { te_problem.eval_hess_ψ(x, y, Σ, 1., v); };
415 cvt.convert_values(eval_h, hess_ψ.reshaped());
416 mat fd_hess_ψ = finite_diff_hess(
417 [&](crvec x, rvec g) {
418 te_problem.eval_grad_ψ(x, y, Σ, g, wn, wm);
419 },
420 x);
421 print_compare(hess_ψ, fd_hess_ψ);
422 }
423}
The main polymorphic minimization problem interface.
void eval_grad_f_grad_g_prod(crvec x, crvec y, rvec grad_f, rvec grad_gxy) const
[Optional] Evaluate both and .
void eval_grad_f(crvec x, rvec grad_fx) const
[Required] Function that evaluates the gradient of the cost,
#define USING_ALPAQA_CONFIG(Conf)
Definition config.hpp:77
std::string demangled_typename(const std::type_info &t)
Get the pretty name of the given type as a string.
auto finite_diff(const std::function< real_t(crvec)> &f, crvec x)
auto finite_diff_hess(const std::function< void(crvec, rvec)> &grad_L, crvec x)
const auto * docs
auto get_problem_path(const char *const *argv)
void print_usage(const char *a0)
void default_eval_grad_L(const alpaqa::TypeErasedProblem< config_t > problem, crvec x, crvec y, rvec grad_L, rvec work_n)
auto split_once(std::string_view s, char tok='.')
Split the string s on the first occurrence of tok.
int main(int argc, const char *argv[])
auto finite_diff_hess_sparse(const std::function< void(crvec, rvec)> &grad_L, crvec x)
auto default_eval_ψ_grad_ψ(const alpaqa::TypeErasedProblem< config_t > &problem, crvec x, crvec y, crvec Σ, rvec grad_ψ, rvec work_n, rvec work_m) -> real_t
void check_gradients(LoadedProblem &, std::ostream &, const CheckGradientsOpts &)
void print_provided_functions(std::ostream &os, const TypeErasedProblem< Conf > &problem)
std::ostream & print_python(std::ostream &os, const Eigen::DenseBase< Derived > &M, Args &&...args)
Definition print.hpp:69
std::string float_to_str(F value, int precision)
Definition print.tpp:67
void set_params(T &t, std::string_view prefix, Options &opts)
Definition options.hpp:126
LoadedProblem load_problem(std::string_view type, const fs::path &dir, const fs::path &file, Options &opts)
Definition problem.cpp:194
vec initial_guess_y
Multipliers g.
Definition problem.hpp:31
vec initial_guess_x
Unknowns.
Definition problem.hpp:30
alpaqa::TypeErasedProblem< config_t > problem
Definition problem.hpp:25
Double-precision double configuration.
Definition config.hpp:176