alpaqa 1.0.0a18
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
112
113void check_gradients(LoadedProblem &, std::ostream &,
114 const CheckGradientsOpts &);
115
116int main(int argc, const char *argv[]) try {
117#ifdef _WIN32
118 SetConsoleOutputCP(CP_UTF8);
119#endif
120 // Check command line options
121 if (argc < 1)
122 return -1;
123 if (argc == 1)
124 return print_usage(argv[0]), 0;
125 if (argc < 2)
126 return print_usage(argv[0]), -1;
127 std::span args{argv, static_cast<size_t>(argc)};
128 Options opts{argc - 2, argv + 2};
129
130 // Check where to write the output to
131 std::ostream &os = std::cout;
132
133 // Check which problem to load
134 auto [prob_path, prob_type] = get_problem_path(argv);
135
136 // Load problem
137 os << "Loading problem " << prob_path << std::endl;
138 auto problem = load_problem(prob_type, prob_path.parent_path(),
139 prob_path.filename(), opts);
140 os << "Loaded problem " << problem.path.stem().string() << " from "
141 << problem.path << "\nnvar: " << problem.problem.get_n()
142 << "\nncon: " << problem.problem.get_m() << "\nProvided functions:\n";
143 alpaqa::print_provided_functions(os, problem.problem);
144 os << std::endl;
145
146 // Options
147 auto has_opt = [&opts](std::string_view o) {
148 auto o_it = std::ranges::find(opts.options(), o);
149 if (o_it == opts.options().end())
150 return false;
151 auto index = static_cast<size_t>(o_it - opts.options().begin());
152 ++opts.used()[index];
153 return true;
154 };
155 CheckGradientsOpts cg_opts{
156 .print_full = has_opt("--full-print"),
157 .hessians = !has_opt("--no-hessians"),
158 };
159
160 // Seed rand
161 auto seed = static_cast<unsigned int>(std::time(nullptr));
162 set_params(seed, "--seed", opts);
163 std::srand(seed);
164
165 // Check options
166 auto used = opts.used();
167 auto unused_opt = std::ranges::find(used, 0);
168 auto unused_idx = static_cast<size_t>(unused_opt - used.begin());
169 if (unused_opt != used.end())
170 throw std::invalid_argument("Unused option: " +
171 std::string(opts.options()[unused_idx]));
172
173 // Check gradients
174 check_gradients(problem, os, cg_opts);
175
176} catch (std::exception &e) {
177 std::cerr << "Error: " << demangled_typename(typeid(e)) << ":\n "
178 << e.what() << std::endl;
179 return -1;
180}
181
182auto finite_diff(const std::function<real_t(crvec)> &f, crvec x) {
183 const auto n = x.size();
184 vec grad(n);
185 vec h = vec::Zero(n);
186 const auto ε = 5e-6;
187 const auto δ = 1e-2 * ε;
188 const real_t fx = f(x);
189 for (index_t i = 0; i < n; ++i) {
190 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
191 h(i) = hh;
192 grad.coeffRef(i) = (f(x + h) - fx) / hh;
193 h(i) = 0;
194 }
195 return std::make_tuple(fx, std::move(grad));
196}
197
198auto finite_diff_hess_sparse(const std::function<void(crvec, rvec)> &grad_L,
199 crvec x) {
200 const auto n = x.size();
201 std::vector<Eigen::Triplet<real_t, index_t>> coo;
202 vec h = vec::Zero(n);
203 const auto ε = 5e-6;
204 const auto δ = 1e-2 * ε;
205 vec grad_x(n), grad_xh(n);
206 grad_L(x, grad_x);
207 for (index_t i = 0; i < n; ++i) {
208 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
209 h(i) = hh;
210 grad_L(x + h, grad_xh);
211 grad_xh = (grad_xh - grad_x) / hh;
212 for (index_t j = 0; j < n; ++j)
213 if (real_t v = grad_xh(j); v != 0)
214 coo.emplace_back(std::min(j, i), std::max(i, j),
215 v * (i == j ? 1 : 0.5));
216 h(i) = 0;
217 }
218 Eigen::SparseMatrix<real_t, 0, index_t> hess(n, n);
219 hess.setFromTriplets(coo.begin(), coo.end());
220 return hess;
221}
222
223auto finite_diff_hess(const std::function<void(crvec, rvec)> &grad_L, crvec x) {
224 const auto n = x.size();
225 vec h = vec::Zero(n);
226 const auto ε = 5e-6;
227 const auto δ = 1e-2 * ε;
228 vec grad_x(n), grad_xh(n);
229 mat hess(n, n);
230 grad_L(x, grad_x);
231 for (index_t i = 0; i < n; ++i) {
232 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
233 h(i) = hh;
234 grad_L(x + h, grad_xh);
235 hess.col(i) = (grad_xh - grad_x) / hh;
236 h(i) = 0;
237 }
238 return hess;
239}
240
241void check_gradients(LoadedProblem &lproblem, std::ostream &log,
242 const CheckGradientsOpts &opts) {
243 auto &te_problem = lproblem.problem;
244
245 auto x0 = lproblem.initial_guess_x;
246 auto y0 = lproblem.initial_guess_y;
247 auto sc = 1e-2 + x0.norm();
248 auto n = te_problem.get_n(), m = te_problem.get_m();
249
250 std::srand(static_cast<unsigned int>(std::time(nullptr)));
251 vec Σ = 1.5 * vec::Random(m).array() + 2;
252 vec y = y0 + (1e-2 + y0.norm()) * vec::Random(m);
253 vec x = x0 + sc * vec::Random(n);
254 vec v = 5e-6 * sc * vec::Random(n);
255 vec gx(m);
256 vec wn(n), wm(m);
257
258 auto print_compare = [&log, &opts](const auto &fd, const auto &ad) {
259 auto abs_err = (fd - ad).reshaped().template lpNorm<Eigen::Infinity>();
260 auto rel_err =
261 abs_err / fd.reshaped().template lpNorm<Eigen::Infinity>();
262 log << " abs error = " << alpaqa::float_to_str(abs_err) << '\n';
263 log << " rel error = " << alpaqa::float_to_str(rel_err) << '\n';
264 if (opts.print_full) {
265 alpaqa::print_python(log << " fd = ", fd);
266 alpaqa::print_python(log << " ad = ", ad) << std::endl;
267 }
268 };
269 auto print_compare_scal = [&log, &opts](const auto &fd, const auto &ad) {
270 auto abs_err = std::abs(fd - ad);
271 auto rel_err = abs_err / std::abs(fd);
272 log << " abs error = " << alpaqa::float_to_str(abs_err) << '\n';
273 log << " rel error = " << alpaqa::float_to_str(rel_err) << '\n';
274 if (opts.print_full) {
275 log << " fd = " << alpaqa::float_to_str(fd) << '\n';
276 log << " ad = " << alpaqa::float_to_str(ad) << '\n' << std::endl;
277 }
278 };
279
280 auto f = [&](crvec x) { return te_problem.eval_f(x); };
281 log << "Gradient verification: ∇f(x)\n";
282 auto [fx, fd_grad_f] = finite_diff(f, x);
283 vec grad_f(n);
284 te_problem.eval_grad_f(x, grad_f);
285 print_compare(fd_grad_f, grad_f);
286
287 if (te_problem.provides_eval_f_grad_f()) {
288 log << "Gradient verification: ∇f(x) (f_grad_f)\n";
289 vec f_grad_f(n);
290 auto f2 = te_problem.eval_f_grad_f(x, f_grad_f);
291 print_compare(fd_grad_f, f_grad_f);
292 log << "Function verification: f(x) (f_grad_f)\n";
293 print_compare_scal(fx, f2);
294 }
295
296 log << "Gradient verification: ∇L(x)\n";
297 auto L = [&](crvec x) {
298 te_problem.eval_g(x, gx);
299 return te_problem.eval_f(x) + gx.dot(y);
300 };
301 auto [Lx, fd_grad_L] = finite_diff(L, x);
302 vec grad_L(n);
303 te_problem.eval_grad_L(x, y, grad_L, wn);
304 print_compare(fd_grad_L, grad_L);
305
306 log << "Gradient verification: ∇ψ(x)\n";
307 auto ψ = [&](crvec x) { return te_problem.eval_ψ(x, y, Σ, wm); };
308 auto [ψx, fd_grad_ψ] = finite_diff(ψ, x);
309 vec grad_ψ(n);
310 te_problem.eval_grad_ψ(x, y, Σ, grad_ψ, wn, wm);
311 print_compare(fd_grad_ψ, grad_ψ);
312
313 if (te_problem.provides_eval_ψ_grad_ψ()) {
314 log << "Gradient verification: ∇ψ(x) (ψ_grad_ψ)\n";
315 vec ψ_grad_ψ(n);
316 real_t ψ2 = te_problem.eval_ψ_grad_ψ(x, y, Σ, ψ_grad_ψ, wn, wm);
317 log << "Function verification: f(x) (f_grad_f)\n";
318 print_compare(fd_grad_f, ψ_grad_ψ);
319 print_compare_scal(ψx, ψ2);
320 }
321
322 if (te_problem.provides_eval_hess_L_prod()) {
323 log << "Hessian product verification: ∇²L(x)\n";
324 vec grad_Lv(n);
325 vec xv = x + v;
326 te_problem.eval_grad_L(xv, y, grad_Lv, wn);
327 vec fd_hess_Lv = grad_Lv - grad_L;
328 vec hess_Lv(n);
329 te_problem.eval_hess_L_prod(x, y, 1, v, hess_Lv);
330 print_compare(fd_hess_Lv, hess_Lv);
331 }
332
333 if (te_problem.provides_eval_hess_ψ_prod()) {
334 log << "Hessian product verification: ∇²ψ(x)\n";
335 vec grad_ψv(n);
336 vec xv = x + v;
337 te_problem.eval_grad_ψ(xv, y, Σ, grad_ψv, wn, wm);
338 vec fd_hess_ψv = grad_ψv - grad_ψ;
339 vec hess_ψv(n);
340 te_problem.eval_hess_ψ_prod(x, y, Σ, 1, v, hess_ψv);
341 print_compare(fd_hess_ψv, hess_ψv);
342 }
343
344 if (opts.hessians && te_problem.provides_eval_hess_L()) {
345 log << "Hessian verification: ∇²L(x)\n";
346 namespace sp = alpaqa::sparsity;
347 auto sparsity = te_problem.get_hess_L_sparsity();
348 sp::SparsityConverter<sp::Sparsity<config_t>, sp::Dense<config_t>> cvt{
349 sparsity};
350 mat hess_L(n, n);
351 auto eval_h = [&](rvec v) { te_problem.eval_hess_L(x, y, 1., v); };
352 cvt.convert_values(eval_h, hess_L.reshaped());
353 mat fd_hess_L = finite_diff_hess(
354 [&](crvec x, rvec g) { te_problem.eval_grad_L(x, y, g, wn); }, x);
355 print_compare(fd_hess_L, hess_L);
356 }
357
358 if (opts.hessians && te_problem.provides_eval_hess_ψ()) {
359 log << "Hessian verification: ∇²ψ(x)\n";
360 namespace sp = alpaqa::sparsity;
361 auto sparsity = te_problem.get_hess_ψ_sparsity();
362 sp::SparsityConverter<sp::Sparsity<config_t>, sp::Dense<config_t>> cvt{
363 sparsity};
364 mat hess_ψ(n, n);
365 auto eval_h = [&](rvec v) { te_problem.eval_hess_ψ(x, y, Σ, 1., v); };
366 cvt.convert_values(eval_h, hess_ψ.reshaped());
367 mat fd_hess_ψ = finite_diff_hess(
368 [&](crvec x, rvec g) {
369 te_problem.eval_grad_ψ(x, y, Σ, g, wn, wm);
370 },
371 x);
372 print_compare(fd_hess_ψ, hess_ψ);
373 }
374}
#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)
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)
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:128
LoadedProblem load_problem(std::string_view type, const fs::path &dir, const fs::path &file, Options &opts)
Definition problem.cpp:183
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:174