alpaqa cmake-targets
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_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_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
182vec 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 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 = 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 + 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_matlab(log << " fd = ", fd);
266 alpaqa::print_matlab(log << " ad = ", ad) << std::endl;
267 }
268 };
269
270 auto f = [&](crvec x) { return te_problem.eval_f(x); };
271 log << "Gradient verification: ∇f(x)\n";
272 vec fd_grad_f = finite_diff(f, x);
273 vec grad_f(n);
274 te_problem.eval_grad_f(x, grad_f);
275 print_compare(fd_grad_f, grad_f);
276
277 log << "Gradient verification: ∇L(x)\n";
278 auto L = [&](crvec x) {
279 te_problem.eval_g(x, gx);
280 return te_problem.eval_f(x) + gx.dot(y);
281 };
282 vec fd_grad_L = finite_diff(L, x);
283 vec grad_L(n);
284 te_problem.eval_grad_L(x, y, grad_L, wn);
285 print_compare(fd_grad_L, grad_L);
286
287 log << "Gradient verification: ∇ψ(x)\n";
288 auto ψ = [&](crvec x) { return te_problem.eval_ψ(x, y, Σ, wm); };
289 vec fd_grad_ψ = finite_diff(ψ, x);
290 vec grad_ψ(n);
291 te_problem.eval_grad_ψ(x, y, Σ, grad_ψ, wn, wm);
292 print_compare(fd_grad_ψ, grad_ψ);
293
294 if (te_problem.provides_eval_hess_L_prod()) {
295 log << "Hessian product verification: ∇²L(x)\n";
296 vec grad_Lv(n);
297 vec xv = x + v;
298 te_problem.eval_grad_L(xv, y, grad_Lv, wn);
299 vec fd_hess_Lv = grad_Lv - grad_L;
300 vec hess_Lv(n);
301 te_problem.eval_hess_L_prod(x, y, 1, v, hess_Lv);
302 print_compare(fd_hess_Lv, hess_Lv);
303 }
304
305 if (te_problem.provides_eval_hess_ψ_prod()) {
306 log << "Hessian product verification: ∇²ψ(x)\n";
307 vec grad_ψv(n);
308 vec xv = x + v;
309 te_problem.eval_grad_ψ(xv, y, Σ, grad_ψv, wn, wm);
310 vec fd_hess_ψv = grad_ψv - grad_ψ;
311 vec hess_ψv(n);
312 te_problem.eval_hess_ψ_prod(x, y, Σ, 1, v, hess_ψv);
313 print_compare(fd_hess_ψv, hess_ψv);
314 }
315
316 if (opts.hessians && te_problem.provides_eval_hess_L()) {
317 log << "Hessian verification: ∇²L(x)\n";
318 namespace sp = alpaqa::sparsity;
319 auto sparsity = te_problem.get_hess_L_sparsity();
320 sp::SparsityConverter<sp::Sparsity<config_t>, sp::Dense<config_t>> cvt{
321 sparsity};
322 mat hess_L(n, n);
323 auto eval_h = [&](rvec v) { te_problem.eval_hess_L(x, y, 1., v); };
324 cvt.convert_values(eval_h, hess_L.reshaped());
325 mat fd_hess_L = finite_diff_hess(
326 [&](crvec x, rvec g) { te_problem.eval_grad_L(x, y, g, wn); }, x);
327 print_compare(fd_hess_L, hess_L);
328 }
329
330 if (opts.hessians && te_problem.provides_eval_hess_ψ()) {
331 log << "Hessian verification: ∇²ψ(x)\n";
332 namespace sp = alpaqa::sparsity;
333 auto sparsity = te_problem.get_hess_ψ_sparsity();
334 sp::SparsityConverter<sp::Sparsity<config_t>, sp::Dense<config_t>> cvt{
335 sparsity};
336 mat hess_ψ(n, n);
337 auto eval_h = [&](rvec v) { te_problem.eval_hess_ψ(x, y, Σ, 1., v); };
338 cvt.convert_values(eval_h, hess_ψ.reshaped());
339 mat fd_hess_ψ = finite_diff_hess(
340 [&](crvec x, rvec g) {
341 te_problem.eval_grad_ψ(x, y, Σ, g, wn, wm);
342 },
343 x);
344 print_compare(fd_hess_ψ, hess_ψ);
345 }
346}
#define USING_ALPAQA_CONFIG(Conf)
Definition config.hpp:56
std::string demangled_typename(const std::type_info &t)
Get the pretty name of the given type as a string.
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[])
vec finite_diff(const std::function< real_t(crvec)> &f, crvec x)
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::string float_to_str(F value, int precision)
Definition print.tpp:67
std::ostream & print_matlab(std::ostream &os, const Eigen::DenseBase< Derived > &M, Args &&...args)
Definition print.hpp:68
decltype(auto) set_params(T &t, std::string_view prefix, Options &opts)
Definition options.hpp:86
LoadedProblem load_problem(std::string_view type, const fs::path &dir, const fs::path &file, Options &opts)
Definition problem.cpp:189
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:135