alpaqa 1.0.0a13
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#include "util.hpp"
15
16#include <Eigen/Sparse>
17
18#ifdef ALPAQA_HAVE_CASADI
19#include <casadi/config.h>
20#endif
21#ifdef WITH_IPOPT
22#include <IpoptConfig.h>
23#endif
24
25#include <algorithm>
26#include <filesystem>
27#include <fstream>
28#include <iostream>
29#include <random>
30#include <span>
31#include <stdexcept>
32#include <string>
33#include <string_view>
34#include <tuple>
35#include <type_traits>
36namespace fs = std::filesystem;
37
39
40const auto *docs = R"==(
41problem types:
42 dl: Dynamically loaded problem using the DLProblem class.
43 Specify the name of the registration function using the
44 problem.register option, e.g. problem.register=register_alpaqa_problem.
45 Further options can be passed to the problem using
46 problem.<key>[=<value>].
47 cs: Load a CasADi problem using the CasADiProblem class.
48 If a .tsv file with the same name as the shared library file exists,
49 the bounds and parameters will be loaded from that file. See
50 CasADiProblem::load_numerical_data for more details.
51 The problem parameter can be set using the problem.param option.
52 cu: Load a CUTEst problem using the CUTEstProblem class.
53
54options:
55 --full-print
56 Print the full gradients.
57 --no-hessians
58 Do not check any Hessian matrices.
59 --seed=<seed>
60 Seed for the random number generator.
61)==";
62
63void print_usage(const char *a0) {
64 const auto *opts = " [<problem-type>:][<path>/]<name> [options...]\n";
65 std::cout << "alpaqa gradient-checker (" ALPAQA_VERSION_FULL ")\n\n"
66 " Command-line utility to check problem gradients.\n"
67 " alpaqa is published under the LGPL-3.0.\n"
68 " https://github.com/kul-optec/alpaqa"
69 "\n\n"
70 " Usage: "
71 << a0 << opts << docs << std::endl;
72 std::cout << "Third-party libraries:\n"
73 << " * Eigen " << EIGEN_WORLD_VERSION << '.'
74 << EIGEN_MAJOR_VERSION << '.' << EIGEN_MINOR_VERSION
75 << " (https://gitlab.com/libeigen/eigen) - MPL-2.0\n"
76#ifdef ALPAQA_HAVE_CASADI
77 << " * CasADi " CASADI_VERSION_STRING
78 " (https://github.com/casadi/casadi) - LGPL-3.0-or-later\n"
79#endif
80#ifdef ALPAQA_HAVE_CUTEST
81 << " * CUTEst"
82 " (https://github.com/ralna/CUTEst) - BSD-3-Clause\n"
83#endif
84 << std::endl;
85}
86
87/// Split the string @p s on the first occurrence of @p tok.
88/// Returns ("", s) if tok was not found.
89auto split_once(std::string_view s, char tok = '.') {
90 auto tok_pos = s.find(tok);
91 if (tok_pos == s.npos)
92 return std::make_tuple(std::string_view{}, s);
93 std::string_view key{s.begin(), s.begin() + tok_pos};
94 std::string_view rem{s.begin() + tok_pos + 1, s.end()};
95 return std::make_tuple(key, rem);
96}
97
98auto get_problem_path(const char *const *argv) {
99 bool rel_to_exe = argv[1][0] == '^';
100 std::string_view prob_path_s = argv[1] + static_cast<ptrdiff_t>(rel_to_exe);
101 std::string_view prob_type;
102 std::tie(prob_type, prob_path_s) = split_once(prob_path_s, ':');
103 fs::path prob_path{prob_path_s};
104 if (rel_to_exe)
105 prob_path = fs::canonical(fs::path(argv[0])).parent_path() / prob_path;
106 return std::make_tuple(std::move(prob_path), prob_type);
107}
108
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 };
160
161 // Seed rand
162 auto seed = static_cast<unsigned int>(std::time(nullptr));
163 set_params(seed, "--seed", opts);
164 std::srand(seed);
165
166 // Check options
167 auto used = opts.used();
168 auto unused_opt = std::ranges::find(used, 0);
169 auto unused_idx = static_cast<size_t>(unused_opt - used.begin());
170 if (unused_opt != used.end())
171 throw std::invalid_argument("Unused option: " +
172 std::string(opts.options()[unused_idx]));
173
174 // Check gradients
175 check_gradients(problem, os, cg_opts);
176
177} catch (std::exception &e) {
178 std::cerr << "Error: " << demangled_typename(typeid(e)) << ":\n "
179 << e.what() << std::endl;
180 return -1;
181}
182
183vec finite_diff(const std::function<real_t(crvec)> &f, crvec x) {
184 const auto n = x.size();
185 vec grad(n);
186 vec h = vec::Zero(n);
187 const auto ε = 5e-6;
188 const auto δ = 1e-2 * ε;
189 const real_t fx = f(x);
190 for (index_t i = 0; i < n; ++i) {
191 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
192 h(i) = hh;
193 grad.coeffRef(i) = (f(x + h) - fx) / hh;
194 h(i) = 0;
195 }
196 return grad;
197}
198
199auto finite_diff_hess_sparse(const std::function<void(crvec, rvec)> &grad_L,
200 crvec x) {
201 const auto n = x.size();
202 std::vector<Eigen::Triplet<real_t, index_t>> coo;
203 vec h = vec::Zero(n);
204 const auto ε = 5e-6;
205 const auto δ = 1e-2 * ε;
206 vec grad_x(n), grad_xh(n);
207 grad_L(x, grad_x);
208 for (index_t i = 0; i < n; ++i) {
209 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
210 h(i) = hh;
211 grad_L(x + h, grad_xh);
212 grad_xh = (grad_xh - grad_x) / hh;
213 for (index_t j = 0; j < n; ++j)
214 if (real_t v = grad_xh(j); v != 0)
215 coo.emplace_back(std::min(j, i), std::max(i, j),
216 v * (i == j ? 1 : 0.5));
217 h(i) = 0;
218 }
219 Eigen::SparseMatrix<real_t, 0, index_t> hess(n, n);
220 hess.setFromTriplets(coo.begin(), coo.end());
221 return hess;
222}
223
224auto finite_diff_hess(const std::function<void(crvec, rvec)> &grad_L, crvec x) {
225 const auto n = x.size();
226 vec h = vec::Zero(n);
227 const auto ε = 5e-6;
228 const auto δ = 1e-2 * ε;
229 vec grad_x(n), grad_xh(n);
230 mat hess(n, n);
231 grad_L(x, grad_x);
232 for (index_t i = 0; i < n; ++i) {
233 real_t hh = std::abs(x(i)) * ε > δ ? x(i) * ε : δ;
234 h(i) = hh;
235 grad_L(x + h, grad_xh);
236 hess.col(i) = (grad_xh - grad_x) / hh;
237 h(i) = 0;
238 }
239 return hess;
240}
241
242void check_gradients(LoadedProblem &lproblem, std::ostream &log,
243 const CheckGradientsOpts &opts) {
244 auto &te_problem = lproblem.problem;
245
246 auto x0 = lproblem.initial_guess_x;
247 auto y0 = lproblem.initial_guess_y;
248 auto sc = x0.norm();
249 auto n = te_problem.get_n(), m = te_problem.get_m();
250
251 std::srand(static_cast<unsigned int>(std::time(nullptr)));
252 vec Σ = 1.5 * vec::Random(m).array() + 2;
253 vec y = y0 + y0.norm() * vec::Random(m);
254 vec x = x0 + sc * vec::Random(n);
255 vec v = 5e-6 * sc * vec::Random(n);
256 vec gx(m);
257 vec wn(n), wm(m);
258
259 auto print_compare = [&log, &opts](const auto &fd, const auto &ad) {
260 auto abs_err = (fd - ad).reshaped().template lpNorm<Eigen::Infinity>();
261 auto rel_err =
262 abs_err / fd.reshaped().template lpNorm<Eigen::Infinity>();
263 log << " abs error = " << alpaqa::float_to_str(abs_err) << '\n';
264 log << " rel error = " << alpaqa::float_to_str(rel_err) << '\n';
265 if (opts.print_full) {
266 alpaqa::print_matlab(log << " fd = ", fd);
267 alpaqa::print_matlab(log << " ad = ", ad) << std::endl;
268 }
269 };
270
271 auto f = [&](crvec x) { return te_problem.eval_f(x); };
272 log << "Gradient verification: ∇f(x)\n";
273 vec fd_grad_f = finite_diff(f, x);
274 vec grad_f(n);
275 te_problem.eval_grad_f(x, grad_f);
276 print_compare(fd_grad_f, grad_f);
277
278 log << "Gradient verification: ∇L(x)\n";
279 auto L = [&](crvec x) {
280 te_problem.eval_g(x, gx);
281 return te_problem.eval_f(x) + gx.dot(y);
282 };
283 vec fd_grad_L = finite_diff(L, x);
284 vec grad_L(n);
285 te_problem.eval_grad_L(x, y, grad_L, wn);
286 print_compare(fd_grad_L, grad_L);
287
288 log << "Gradient verification: ∇ψ(x)\n";
289 auto ψ = [&](crvec x) { return te_problem.eval_ψ(x, y, Σ, wm); };
290 vec fd_grad_ψ = finite_diff(ψ, x);
291 vec grad_ψ(n);
292 te_problem.eval_grad_ψ(x, y, Σ, grad_ψ, wn, wm);
293 print_compare(fd_grad_ψ, grad_ψ);
294
295 if (te_problem.provides_eval_hess_L_prod()) {
296 log << "Hessian product verification: ∇²L(x)\n";
297 vec grad_Lv(n);
298 vec xv = x + v;
299 te_problem.eval_grad_L(xv, y, grad_Lv, wn);
300 vec fd_hess_Lv = grad_Lv - grad_L;
301 vec hess_Lv(n);
302 te_problem.eval_hess_L_prod(x, y, 1, v, hess_Lv);
303 print_compare(fd_hess_Lv, hess_Lv);
304 }
305
306 if (te_problem.provides_eval_hess_ψ_prod()) {
307 log << "Hessian product verification: ∇²ψ(x)\n";
308 vec grad_ψv(n);
309 vec xv = x + v;
310 te_problem.eval_grad_ψ(xv, y, Σ, grad_ψv, wn, wm);
311 vec fd_hess_ψv = grad_ψv - grad_ψ;
312 vec hess_ψv(n);
313 te_problem.eval_hess_ψ_prod(x, y, Σ, 1, v, hess_ψv);
314 print_compare(fd_hess_ψv, hess_ψv);
315 }
316
317 if (opts.hessians && te_problem.provides_eval_hess_L()) {
318 log << "Hessian verification: ∇²L(x)\n";
319 namespace sp = alpaqa::sparsity;
320 auto sparsity = te_problem.get_hess_L_sparsity();
321 sp::SparsityConverter<sp::Sparsity<config_t>, sp::Dense<config_t>> cvt{
322 sparsity};
323 mat hess_L(n, n);
324 auto eval_h = [&](rvec v) { te_problem.eval_hess_L(x, y, 1., v); };
325 cvt.convert_values(eval_h, hess_L.reshaped());
326 mat fd_hess_L = finite_diff_hess(
327 [&](crvec x, rvec g) { te_problem.eval_grad_L(x, y, g, wn); }, x);
328 print_compare(fd_hess_L, hess_L);
329 }
330
331 if (opts.hessians && te_problem.provides_eval_hess_ψ()) {
332 log << "Hessian verification: ∇²ψ(x)\n";
333 namespace sp = alpaqa::sparsity;
334 auto sparsity = te_problem.get_hess_ψ_sparsity();
335 sp::SparsityConverter<sp::Sparsity<config_t>, sp::Dense<config_t>> cvt{
336 sparsity};
337 mat hess_ψ(n, n);
338 auto eval_h = [&](rvec v) { te_problem.eval_hess_ψ(x, y, Σ, 1., v); };
339 cvt.convert_values(eval_h, hess_ψ.reshaped());
340 mat fd_hess_ψ = finite_diff_hess(
341 [&](crvec x, rvec g) {
342 te_problem.eval_grad_ψ(x, y, Σ, g, wn, wm);
343 },
344 x);
345 print_compare(fd_hess_ψ, hess_ψ);
346 }
347}
#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:27
LoadedProblem load_problem(std::string_view type, const fs::path &dir, const fs::path &file, Options &opts)
Definition problem.cpp:152
vec initial_guess_y
Unknowns.
Definition problem.hpp:21
vec initial_guess_x
Definition problem.hpp:20
alpaqa::TypeErasedProblem< config_t > problem
Definition problem.hpp:15
Double-precision double configuration.
Definition config.hpp:135