alpaqa dll
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
9#include <guanaqo/demangled-typename.hpp>
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_num_variables()
143 << "\nncon: " << problem.problem.get_num_constraints()
144 << "\nProvided functions:\n";
145 alpaqa::print_provided_functions(os, problem.problem);
146 os << std::endl;
147
148 // Options
149 auto has_opt = [&opts](std::string_view o) {
150 auto o_it = std::ranges::find(opts.options(), o);
151 if (o_it == opts.options().end())
152 return false;
153 auto index = static_cast<size_t>(o_it - opts.options().begin());
154 ++opts.used()[index];
155 return true;
156 };
157 CheckGradientsOpts cg_opts{
158 .print_full = has_opt("--full-print"),
159 .hessians = !has_opt("--no-hessians"),
160 .scale_perturbations = 1e-2,
161 };
162 set_params(cg_opts.scale_perturbations, "--scale-perturbations", opts);
163
164 // Seed rand
165 auto seed = static_cast<unsigned int>(std::time(nullptr));
166 set_params(seed, "--seed", opts);
167 std::srand(seed);
168
169 // Check options
170 auto used = opts.used();
171 auto unused_opt = std::ranges::find(used, 0);
172 auto unused_idx = static_cast<size_t>(unused_opt - used.begin());
173 if (unused_opt != used.end())
174 throw std::invalid_argument("Unused option: " +
175 std::string(opts.options()[unused_idx]));
176
177 // Check gradients
178 check_gradients(problem, os, cg_opts);
179
180} catch (std::exception &e) {
181 std::cerr << "Error: " << guanaqo::demangled_typename(typeid(e)) << ":\n "
182 << e.what() << std::endl;
183 return -1;
184}
185
187 const alpaqa::TypeErasedProblem<config_t> problem, crvec x, crvec y,
188 rvec grad_L, rvec work_n) {
189 if (y.size() == 0) /* [[unlikely]] */
190 return problem.eval_objective_gradient(x, grad_L);
192 x, y, grad_L, work_n);
193 grad_L += work_n;
194}
195
197 const alpaqa::TypeErasedProblem<config_t> &problem, crvec x, crvec y,
198 crvec Σ, rvec grad_ψ, rvec work_n, rvec work_m) -> real_t {
199 if (y.size() == 0) /* [[unlikely]] */
200 return problem.eval_objective_and_gradient(x, grad_ψ);
201
202 auto &ŷ = work_m;
203 // ψ(x) = f(x) + ½ dᵀŷ
204 auto f = problem.eval_objective_and_constraints(x, ŷ);
205 auto dᵀŷ = problem.calc_ŷ_dᵀŷ(ŷ, y, Σ);
206 auto ψ = f + real_t(0.5) * dᵀŷ;
207 // ∇ψ(x) = ∇f(x) + ∇g(x) ŷ
208 try {
209 default_eval_lagrangian_gradient(problem, x, ŷ, grad_ψ, work_n);
210 } catch (alpaqa::not_implemented_error &) {
211 problem.eval_lagrangian_gradient(x, ŷ, grad_ψ, work_n);
212 }
213 return ψ;
214}
215
216auto finite_diff(const std::function<real_t(crvec)> &f, crvec x) {
217 const auto n = x.size();
218 vec grad(n);
219 vec h = vec::Zero(n);
220 const auto ε = 5e-6;
221 const auto δ = 1e-2 * ε;
222 const real_t fx = f(x);
223 for (index_t i = 0; i < n; ++i) {
224 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
225 h(i) = hh;
226 grad.coeffRef(i) = (f(x + h) - fx) / hh;
227 h(i) = 0;
228 }
229 return std::make_tuple(fx, std::move(grad));
230}
231
232auto finite_diff_hess_sparse(const std::function<void(crvec, rvec)> &grad_L,
233 crvec x) {
234 const auto n = x.size();
235 std::vector<Eigen::Triplet<real_t, index_t>> coo;
236 vec h = vec::Zero(n);
237 const auto ε = 5e-6;
238 const auto δ = 1e-2 * ε;
239 vec grad_x(n), grad_xh(n);
240 grad_L(x, grad_x);
241 for (index_t i = 0; i < n; ++i) {
242 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
243 h(i) = hh;
244 grad_L(x + h, grad_xh);
245 grad_xh = (grad_xh - grad_x) / hh;
246 for (index_t j = 0; j < n; ++j)
247 if (real_t v = grad_xh(j); v != 0)
248 coo.emplace_back(std::min(j, i), std::max(i, j),
249 v * (i == j ? 1 : 0.5));
250 h(i) = 0;
251 }
252 Eigen::SparseMatrix<real_t, 0, index_t> hess(n, n);
253 hess.setFromTriplets(coo.begin(), coo.end());
254 return hess;
255}
256
257auto finite_diff_hess(const std::function<void(crvec, rvec)> &grad_L, crvec x) {
258 const auto n = x.size();
259 vec h = vec::Zero(n);
260 const auto ε = 5e-6;
261 const auto δ = 1e-2 * ε;
262 vec grad_x(n), grad_xh(n);
263 mat hess(n, n);
264 grad_L(x, grad_x);
265 for (index_t i = 0; i < n; ++i) {
266 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
267 h(i) = hh;
268 grad_L(x + h, grad_xh);
269 hess.col(i) = (grad_xh - grad_x) / hh;
270 h(i) = 0;
271 }
272 return hess;
273}
274
275void check_gradients(LoadedProblem &lproblem, std::ostream &log,
276 const CheckGradientsOpts &opts) {
277 auto &te_problem = lproblem.problem;
278
279 auto x0 = lproblem.initial_guess_x;
280 auto y0 = lproblem.initial_guess_y;
281 auto sc = opts.scale_perturbations + x0.norm();
282 auto n = te_problem.get_num_variables(),
283 m = te_problem.get_num_constraints();
284
285 std::srand(static_cast<unsigned int>(std::time(nullptr)));
286 vec Σ = 1.5 * vec::Random(m).array() + 2;
287 vec y = y0 + (opts.scale_perturbations + y0.norm()) * vec::Random(m);
288 vec x = x0 + sc * vec::Random(n);
289 vec v = 5e-6 * sc * vec::Random(n);
290 vec gx(m);
291 vec wn(n), wm(m);
292
293 auto print_compare = [&log, &opts](const auto &other, const auto &ref) {
294 auto abs_err =
295 (ref - other).reshaped().template lpNorm<Eigen::Infinity>();
296 auto rel_err =
297 abs_err / ref.reshaped().template lpNorm<Eigen::Infinity>();
298 log << " abs error = " << alpaqa::float_to_str(abs_err) << '\n';
299 log << " rel error = " << alpaqa::float_to_str(rel_err) << '\n';
300 if (opts.print_full) {
301 alpaqa::print_python(log << " (1) = ", other);
302 alpaqa::print_python(log << " (2) = ", ref) << std::endl;
303 }
304 };
305 auto print_compare_scal = [&log, &opts](const auto &fd, const auto &ad) {
306 auto abs_err = std::abs(fd - ad);
307 auto rel_err = abs_err / std::abs(fd);
308 log << " abs error = " << alpaqa::float_to_str(abs_err) << '\n';
309 log << " rel error = " << alpaqa::float_to_str(rel_err) << '\n';
310 if (opts.print_full) {
311 log << " (1) = " << alpaqa::float_to_str(fd) << '\n';
312 log << " (2) = " << alpaqa::float_to_str(ad) << '\n' << std::endl;
313 }
314 };
315
316 auto f = [&](crvec x) { return te_problem.eval_objective(x); };
317 log << "Gradient verification: ∇f(x) (grad_f compared to finite "
318 "differences of f)\n";
319 auto [fx, fd_grad_f] = finite_diff(f, x);
320 vec grad_f(n);
321 te_problem.eval_objective_gradient(x, grad_f);
322 print_compare(grad_f, fd_grad_f);
323
324 if (te_problem.provides_eval_objective_and_gradient()) {
325 log << "Gradient verification: ∇f(x) (f_grad_f compared to grad_f)\n";
326 vec f_grad_f(n);
327 auto f2 = te_problem.eval_objective_and_gradient(x, f_grad_f);
328 print_compare(f_grad_f, grad_f);
329 log << "Function verification: f(x) (f_grad_f compared to f)\n";
330 print_compare_scal(f2, fx);
331 }
332
333 log << "Gradient verification: ∇L(x) (grad_L compared to finite "
334 "differences of f + yᵀg)\n";
335 auto L = [&](crvec x) {
336 te_problem.eval_constraints(x, gx);
337 return te_problem.eval_objective(x) + gx.dot(y);
338 };
339 auto [Lx, fd_grad_L] = finite_diff(L, x);
340 vec grad_L(n);
341 te_problem.eval_lagrangian_gradient(x, y, grad_L, wn);
342 print_compare(grad_L, fd_grad_L);
343
344 log << "Gradient verification: ∇ψ(x) (grad_ψ compared to finite "
345 "differences of ψ)\n";
346 auto ψ = [&](crvec x) {
347 return te_problem.eval_augmented_lagrangian(x, y, Σ, wm);
348 };
349 auto [ψx, fd_grad_ψ] = finite_diff(ψ, x);
350 vec grad_ψ(n);
351 te_problem.eval_augmented_lagrangian_gradient(x, y, Σ, grad_ψ, wn, wm);
352 print_compare(grad_ψ, fd_grad_ψ);
353
354 log << "Gradient verification: ∇ψ(x) (grad_ψ compared to reference "
355 "implementation based on g, ∇f, ∇g)\n";
356 vec grad_ψ_default(n);
358 te_problem, x, y, Σ, grad_ψ_default, wn, wm);
359 print_compare(grad_ψ, grad_ψ_default);
360 log << "Function verification: ψ(x) (ψ compared to reference "
361 "implementation based on f, g)\n";
362 print_compare_scal(ψx, ψ_default);
363
364 if (te_problem.provides_eval_augmented_lagrangian_and_gradient()) {
365 log << "Gradient verification: ∇ψ(x) (grad_ψ compared to ψ_grad_ψ)\n";
366 vec ψ_grad_ψ(n);
367 real_t ψ2 = te_problem.eval_augmented_lagrangian_and_gradient(
368 x, y, Σ, ψ_grad_ψ, wn, wm);
369 print_compare(grad_ψ, ψ_grad_ψ);
370 log << "Function verification: ψ(x) (ψ compared to ψ_grad_ψ)\n";
371 print_compare_scal(ψx, ψ2);
372 }
373
374 if (te_problem.provides_eval_lagrangian_hessian_product()) {
375 log << "Hessian product verification: ∇²L(x) (hess_L_prod compared to "
376 "finite differences of grad_L)\n";
377 vec grad_Lv(n);
378 vec xv = x + v;
379 te_problem.eval_lagrangian_gradient(xv, y, grad_Lv, wn);
380 vec fd_hess_Lv = grad_Lv - grad_L;
381 vec hess_Lv(n);
382 te_problem.eval_lagrangian_hessian_product(x, y, 1, v, hess_Lv);
383 print_compare(hess_Lv, fd_hess_Lv);
384 }
385
386 if (te_problem.provides_eval_augmented_lagrangian_hessian_product()) {
387 log << "Hessian product verification: ∇²ψ(x) (hess_ψ_prod compared to "
388 "finite differences of grad_ψ)\n";
389 vec grad_ψv(n);
390 vec xv = x + v;
391 te_problem.eval_augmented_lagrangian_gradient(xv, y, Σ, grad_ψv, wn,
392 wm);
393 vec fd_hess_ψv = grad_ψv - grad_ψ;
394 vec hess_ψv(n);
395 te_problem.eval_augmented_lagrangian_hessian_product(x, y, Σ, 1, v,
396 hess_ψv);
397 print_compare(hess_ψv, fd_hess_ψv);
398 }
399
400 if (opts.hessians && te_problem.provides_eval_lagrangian_hessian()) {
401 log << "Hessian verification: ∇²L(x) (hess_L compared to finite "
402 "differences of grad_L)\n";
403 auto sp = te_problem.get_lagrangian_hessian_sparsity();
405 auto eval_h = [&](rvec v) {
406 te_problem.eval_lagrangian_hessian(x, y, 1., v);
407 };
408 auto hess_L = cvt.eval(eval_h);
409 mat fd_hess_L = finite_diff_hess(
410 [&](crvec x, rvec g) {
411 te_problem.eval_lagrangian_gradient(x, y, g, wn);
412 },
413 x);
414 print_compare(hess_L, fd_hess_L);
415 }
416
417 if (opts.hessians &&
418 te_problem.provides_eval_augmented_lagrangian_hessian()) {
419 log << "Hessian verification: ∇²ψ(x) (hess_ψ compared to finite "
420 "differences of grad_ψ)\\n";
421 auto sp = te_problem.get_augmented_lagrangian_hessian_sparsity();
423 auto eval_h = [&](rvec v) {
424 te_problem.eval_augmented_lagrangian_hessian(x, y, Σ, 1., v);
425 };
426 auto hess_ψ = cvt.eval(eval_h);
427 mat fd_hess_ψ = finite_diff_hess(
428 [&](crvec x, rvec g) {
429 te_problem.eval_augmented_lagrangian_gradient(x, y, Σ, g, wn,
430 wm);
431 },
432 x);
433 print_compare(hess_ψ, fd_hess_ψ);
434 }
435}
const auto * docs
std::span< unsigned > used()
Definition options.hpp:61
std::span< const std::string_view > options() const
Definition options.hpp:58
The main polymorphic minimization problem interface.
void eval_objective_gradient_and_constraints_gradient_product(crvec x, crvec y, rvec grad_f, rvec grad_gxy) const
[Optional] Evaluate both and .
void eval_objective_gradient(crvec x, rvec grad_fx) const
[Required] Function that evaluates the gradient of the cost,
#define USING_ALPAQA_CONFIG(Conf)
Definition config.hpp:77
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)
void default_eval_lagrangian_gradient(const alpaqa::TypeErasedProblem< config_t > problem, crvec x, crvec y, rvec grad_L, rvec work_n)
auto get_problem_path(const char *const *argv)
void print_usage(const char *a0)
auto default_eval_augmented_lagrangian_and_gradient(const alpaqa::TypeErasedProblem< config_t > &problem, crvec x, crvec y, crvec Σ, rvec grad_ψ, rvec work_n, rvec work_m) -> real_t
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)
void check_gradients(LoadedProblem &, std::ostream &, const CheckGradientsOpts &)
void print_provided_functions(std::ostream &os, const TypeErasedProblem< Conf > &problem)
EigenConfigd DefaultConfig
Definition config.hpp:31
std::ostream & print_python(std::ostream &os, const Eigen::DenseBase< Derived > &M)
Definition print.hpp:13
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:197
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