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