alpaqa dll
Nonconvex constrained optimization
Loading...
Searching...
No Matches
alpaqa-driver.cpp
Go to the documentation of this file.
1#ifdef _WIN32
2#define NOMINMAX
3#include <Windows.h>
4#endif
5
10#include <guanaqo/demangled-typename.hpp>
11#include <guanaqo/string-util.hpp>
12#include <alpaqa-version.h>
13
14#include "fista-driver.hpp"
15#include "ipopt-driver.hpp"
16#include "lbfgsb-driver.hpp"
17#include "options.hpp"
18#include "panoc-driver.hpp"
19#include "pantr-driver.hpp"
20#include "param-complete.hpp"
21#include "qpalm-driver.hpp"
22#include "results.hpp"
23#include "solver-driver.hpp"
24
25#ifdef ALPAQA_WITH_EXTERNAL_CASADI
26#include <casadi/config.h>
27#endif
28#ifdef ALPAQA_WITH_JSON
29#include <nlohmann/json_fwd.hpp>
30#endif
31#ifdef ALPAQA_WITH_IPOPT
32#include <IpoptConfig.h>
33#endif
34
35#include <algorithm>
36#include <filesystem>
37#include <fstream>
38#include <iostream>
39#include <random>
40#include <span>
41#include <stdexcept>
42#include <string>
43#include <string_view>
44#include <tuple>
45#include <type_traits>
46namespace fs = std::filesystem;
47using namespace std::string_view_literals;
48
50
51const auto *docs = R"==(
52problem types:
53 dl: Dynamically loaded problem using the DLProblem class.
54 Specify the name of the registration function using the
55 problem.register option, e.g. problem.register=register_alpaqa_problem.
56 Further options can be passed to the problem using
57 problem.<key>[=<value>].
58 cs: Load a CasADi problem using the CasADiProblem class.
59 If a .tsv file with the same name as the shared library file exists,
60 the bounds and parameters will be loaded from that file. See
61 CasADiProblem::load_numerical_data for more details.
62 The problem parameter can be set using the problem.param option.
63 cu: Load a CUTEst problem using the CUTEstProblem class.
64
65methods:
66 panoc[.<direction>]:
67 PANOC solver with the given direction.
68 Directions include: lbfgs, struclbfgs, anderson, convex-newton.
69 zerofpr[.<direction>]:
70 ZeroFPR solver, supports the same directions as PANOC.
71 pantr:
72 PANTR solver. Requires products with the Hessian of the augmented
73 Lagrangian (unless dir.finite_diff=true).
74 fista:
75 FISTA (fast iterative shrinkage-thresholding algorithm). Only for
76 convex problems.
77 ipopt:
78 Ipopt interior point solver. Requires Jacobian of the constraints
79 and Hessian of the Lagrangian (unless finite memory is enabled).
80 qpalm:
81 QPALM proximal ALM QP solver. Assumes that the problem is a QP.
82 Requires Jacobian of the constraints and Hessian of the Lagrangian.
83
84options:
85 Solver-specific options can be specified as key-value pairs, where the
86 keys use periods to access struct members. For example,
87 solver.Lipschitz.L_0=1e3.
88
89 solver: Parameters for the main (inner) solver.
90 alm: Parameters for the outer ALM solver (if applicable).
91 dir: Parameters for solver's direction provider (if applicable).
92 accel: Parameters for direction's accelerator (if applicable).
93 out: File to write output to (default: -, i.e. standard output).
94 sol: Folder to write the solutions (and optional statistics) to.
95 x0: Initial guess for the solution.
96 mul_g0: Initial guess for the multipliers of the general constraints.
97 mul_x0: Initial guess for the multipliers of the bound constraints on x.
98 num_exp: Repeat the experiment this many times for more accurate timings.
99 extra_stats: Log more per-iteration solver statistics, such as step sizes,
100 Newton step acceptance, and residuals. Requires `sol' to be set.
101 show_funcs: Print an overview of the functions provided by the problem.
102 dl_flags: Flags passed to dlopen when loading the problem.
103
104 The prefix @ can be added to the values of x0, mul_g0 and mul_x0 to read
105 the values from the given CSV file.
106
107 Options can be loaded from a JSON file by using an @ prefix. For example,
108 an argument @options.json loads the options from a file options.json in the
109 current directory. Multiple JSON files are processed in the order they
110 appear in the command line arguments. Options specified on the command line
111 always have precedence over options in a JSON file, regardless of order.
112
113examples:
114 alpaqa-driver problem.so \
115 problem.register=register_alpaqa_problem \
116 problem.custom_arg=foo \
117 method=panoc.struclbfgs \
118 accel.memory=50 \
119 alm.{tolerance,dual_tolerance}=1e-8 \
120 solver.print_interval=50 \
121 x0=@/some/file.csv
122
123 alpaqa-driver cs:build/casadi_problem.so \
124 @options/default.json \
125 problem.param=1,2,3 \
126 method=ipopt \
127 solver.tol=1e-8 solver.constr_viol_tol=1e-8 \
128 solver.warm_start_init_point=yes \
129 x0=@/some/file.csv \
130 mul_g0=@/some/other/file.csv \
131 mul_x0=@/yet/another/file.csv
132)==";
133
134void print_usage(const char *a0) {
135 const auto *opts = " [<problem-type>:][<path>/]<name> [method=<solver>] "
136 "[<key>=<value>...]\n";
137 std::cout << "alpaqa-driver " ALPAQA_VERSION_FULL " (" << alpaqa_build_time
138 << ")\n\n"
139 " Command-line interface to the alpaqa solvers.\n"
140 " alpaqa is published under the LGPL-3.0.\n"
141 " https://github.com/kul-optec/alpaqa"
142 "\n\n"
143 " Usage: "
144 << a0 << opts << docs << std::endl;
145 std::cout << "Third-party libraries:\n"
146 << " * Eigen " << EIGEN_WORLD_VERSION << '.'
147 << EIGEN_MAJOR_VERSION << '.' << EIGEN_MINOR_VERSION
148 << " (https://gitlab.com/libeigen/eigen) - MPL-2.0\n"
149#ifdef ALPAQA_WITH_EXTERNAL_CASADI
150 << " * CasADi " CASADI_VERSION_STRING
151 " (https://github.com/casadi/casadi) - LGPL-3.0-or-later\n"
152#endif
153#ifdef ALPAQA_WITH_CUTEST
154 << " * CUTEst"
155 " (https://github.com/ralna/CUTEst) - BSD-3-Clause\n"
156#endif
157#ifdef ALPAQA_WITH_LBFGSB
158 << " * L-BFGS-B 3.0 "
159 "(http://users.iems.northwestern.edu/~nocedal/lbfgsb.html) - "
160 "BSD-3-Clause\n"
161#endif
162#ifdef ALPAQA_WITH_IPOPT
163 << " * Ipopt " IPOPT_VERSION
164 " (https://github.com/coin-or/Ipopt) - EPL-2.0\n"
165 << " * MUMPS (https://mumps-solver.org) - CECILL-C\n"
166 << " * OpenBLAS (https://github.com/OpenMathLib/OpenBLAS) - "
167 "BSD-3-Clause\n"
168#endif
169#ifdef ALPAQA_WITH_QPALM
170 << " * QPALM " QPALM_VERSION_STR
171 " (https://github.com/kul-optec/QPALM) - LGPL-3.0\n"
172#endif
173#ifdef ALPAQA_WITH_JSON
174 << " * nlohmann/json " << NLOHMANN_JSON_VERSION_MAJOR << '.'
175 << NLOHMANN_JSON_VERSION_MINOR << '.'
176 << NLOHMANN_JSON_VERSION_PATCH
177 << " (https://github.com/nlohmann/json) - MIT\n"
178#endif
179 << std::endl;
180}
181
183 std::cout << ALPAQA_VERSION_FULL " (" << ALPAQA_BUILD_TIME << ")\n";
184}
185
186/// Split the string @p s on the first occurrence of @p tok.
187/// Returns ("", s) if tok was not found.
188auto split_once(std::string_view s, char tok = '.') {
189 auto tok_pos = s.find(tok);
190 if (tok_pos == s.npos)
191 return std::make_tuple(std::string_view{}, s);
192 std::string_view key{s.begin(), s.begin() + tok_pos};
193 std::string_view rem{s.begin() + tok_pos + 1, s.end()};
194 return std::make_tuple(key, rem);
195}
196
197std::ostream &get_output_stream(Options &opts, std::ofstream &out_fstream) {
198 std::string out_path = "-";
199 set_params(out_path, "out", opts);
200 if (out_path != "-")
201 if (out_fstream.open(out_path); !out_fstream)
202 throw std::runtime_error("Unable to open '" + out_path + "'");
203 return out_fstream.is_open() ? out_fstream : std::cout;
204}
205
206std::string get_output_paths(Options &opts) {
207 std::string sol_path;
208 set_params(sol_path, "sol", opts);
209 return sol_path;
210}
211
212auto get_problem_path(const char *const *argv) {
213 bool rel_to_exe = argv[1][0] == '^';
214 std::string_view prob_path_s = argv[1] + static_cast<ptrdiff_t>(rel_to_exe);
215 std::string_view prob_type;
216 std::tie(prob_type, prob_path_s) = split_once(prob_path_s, ':');
217 fs::path prob_path{prob_path_s};
218 if (rel_to_exe)
219 prob_path = fs::canonical(fs::path(argv[0])).parent_path() / prob_path;
220 return std::make_tuple(std::move(prob_path), prob_type);
221}
222
223void print_problem_description(std::ostream &os, LoadedProblem &problem,
224 bool show_funcs) {
225 os << "Loaded problem \"" << problem.name << "\"\n"
226 << "Number of variables: " << problem.problem.get_num_variables()
227 << "\n"
228 << "Number of constraints: " << problem.problem.get_num_constraints()
229 << "\n";
230 if (problem.nnz_jac_g)
231 os << "Nonzeros in Jg: " << *problem.nnz_jac_g << "\n";
232 if (problem.nnz_hess_L)
233 os << "Nonzeros in ∇²L: " << *problem.nnz_hess_L << "\n";
234 if (problem.nnz_hess_ψ)
235 os << "Nonzeros in ∇²ψ: " << *problem.nnz_hess_ψ << "\n";
236 if (problem.box_constr_count)
237 os << "Box constraints:" //
238 << "\n Fixed variables: " << problem.box_constr_count->eq
239 << "\n Bilateral: " << problem.box_constr_count->lbub
240 << "\n Lower bound only: " << problem.box_constr_count->lb
241 << "\n Upper bound only: " << problem.box_constr_count->ub
242 << "\n";
243 if (problem.general_constr_count)
244 os << "General constraints:" //
245 << "\n Equality: " << problem.general_constr_count->eq
246 << "\n Bilateral: " << problem.general_constr_count->lbub
247 << "\n Lower bound only: " << problem.general_constr_count->lb
248 << "\n Upper bound only: " << problem.general_constr_count->ub
249 << "\n";
250 if (show_funcs) {
251 os << "Provided functions:\n";
253 }
254}
255
257 std::string method = "panoc", direction;
258 set_params(method, "method", opts);
259 std::tie(method, direction) = alpaqa::params::split_key(method, '.');
260 // Dictionary of available solver builders
261 std::map<std::string_view, solver_builder_func> solvers{
262 {"panoc", make_panoc_driver}, {"zerofpr", make_zerofpr_driver},
263 {"pantr", make_pantr_driver}, {"lbfgsb", make_lbfgsb_driver},
264 {"fista", make_fista_driver}, {"ipopt", make_ipopt_driver},
265 {"qpalm", make_qpalm_driver},
266 };
267 // Find the selected solver builder
268 auto solver_it = solvers.find(method);
269 if (solver_it == solvers.end())
270 throw std::invalid_argument(
271 "Unknown solver '" + std::string(method) + "'\n" +
272 " Available solvers: " + guanaqo::join(std::views::keys(solvers)));
273 ;
274 return std::make_tuple(std::move(solver_it->second), direction);
275}
276
277void store_solution(const fs::path &sol_output_dir, std::ostream &os,
278 BenchmarkResults &results, auto &solver,
279 [[maybe_unused]] const Options &opts,
280 std::span<const char *> argv) {
281 const auto &sol_res = results.solver_results;
282 auto timestamp_str = std::to_string(results.timestamp);
283 auto rnd_str = random_hex_string(std::random_device());
284 auto name = results.problem.path.stem().string();
285 if (name == "PROBLEM")
286 name = results.problem.name;
287 auto suffix = '_' + name + '_' + timestamp_str + '_' + rnd_str;
288 fs::create_directories(sol_output_dir);
289 std::array solutions{
290 std::tuple{"solution", "sol_x", &sol_res.solution},
291 std::tuple{"multipliers for g", "mul_g", &sol_res.multipliers},
292 std::tuple{"multipliers for x", "mul_x", &sol_res.multipliers_bounds},
293 };
294 for (auto [name, fname, value] : solutions) {
295 if (value->size() == 0)
296 continue;
297 auto pth = sol_output_dir / (std::string(fname) + suffix + ".csv");
298 os << "Writing " << name << " to " << pth << std::endl;
299 std::ofstream output_file(pth);
300 guanaqo::print_csv(output_file, alpaqa::as_span(*value));
301 }
302 {
303 auto pth = sol_output_dir / ("cmdline" + suffix + ".txt");
304 os << "Writing arguments to " << pth << std::endl;
305 std::ofstream output_file(pth);
306 for (const char *arg : argv)
307 output_file << std::quoted(arg, '\'') << ' ';
308 output_file << '\n';
309 }
310 if (solver->has_statistics()) {
311 auto pth = sol_output_dir / ("stats" + suffix + ".csv");
312 os << "Writing statistics to " << pth << std::endl;
313 std::ofstream output_file(pth);
314 solver->write_statistics_to_stream(output_file);
315 }
316#if ALPAQA_WITH_JSON
317 {
318 auto pth = sol_output_dir / ("options" + suffix + ".json");
319 os << "Writing options to " << pth << std::endl;
320 std::ofstream output_file(pth);
321 output_file << opts.get_json_out() << '\n';
322 }
323#endif
324}
325
326int main(int argc, const char *argv[]) try {
327#ifdef _WIN32
328 SetConsoleOutputCP(CP_UTF8);
329#endif
330 // Check command line options
331 if (argc < 1)
332 return -1;
333 if (argc == 1)
334 return print_usage(argv[0]), 0;
335 if (argc < 2)
336 return print_usage(argv[0]), -1;
337 if (argv[1] == "-h"sv || argv[1] == "--help"sv || argv[1] == "?"sv)
338 return print_usage(argv[0]), 0;
339 if (argv[1] == "-v"sv || argv[1] == "--version"sv)
340 return print_version(), 0;
341 if (argv[1] == "--complete"sv) {
342 if (argc < 4)
343 return -1;
344 print_completion(argv[2], argv[3]);
345 return 0;
346 }
347
348 std::span args{argv, static_cast<size_t>(argc)};
349 Options opts{argc - 2, argv + 2};
350
351 // Check where to write the output to
352 std::ofstream out_fstream;
353 std::ostream &os = get_output_stream(opts, out_fstream);
354
355 // Check which problem to load
356 auto [prob_path, prob_type] = get_problem_path(argv);
357
358 // Check which solver to use
359 auto [solver_builder, direction] = get_solver_builder(opts);
360
361 // Check output paths
362 fs::path sol_output_dir = get_output_paths(opts);
363
364 // Build solver
365 auto solver = solver_builder(direction, opts);
366
367 // Load problem
368 os << "Loading " << prob_path << " ..." << std::endl;
369 auto problem = load_problem(prob_type, prob_path.parent_path(),
370 prob_path.filename(), opts);
371 // Print problem information
372 bool show_funcs = false;
373 set_params(show_funcs, "show_funcs", opts);
374 print_problem_description(os, problem, show_funcs);
375 os << std::endl;
376
377 // Check options
378 auto used = opts.used();
379 auto unused_opt = std::ranges::find(used, 0);
380 auto unused_idx = static_cast<size_t>(unused_opt - used.begin());
381 if (unused_opt != used.end())
382 throw std::invalid_argument("Unused option: " +
383 std::string(opts.options()[unused_idx]));
384
385 // Solve
386 auto solver_results = solver->run(problem, os);
387
388 // Compute more statistics
389 real_t f = problem.problem.eval_objective(solver_results.solution);
390 auto kkt_err = alpaqa::compute_kkt_error(
391 problem.problem, solver_results.solution, solver_results.multipliers);
392 BenchmarkResults results{
393 .problem = problem,
394 .solver_results = solver_results,
395 .objective = f + solver_results.h,
396 .smooth_objective = f,
397 .error = kkt_err,
398 .options = opts.options(),
399 .timestamp = timestamp_ms<std::chrono::system_clock>().count(),
400 };
401
402 // Print results
403 print_results(os, results);
404
405 // Store solution
406 if (!sol_output_dir.empty())
407 store_solution(sol_output_dir, os, results, solver, opts, args);
408
409} catch (std::exception &e) {
410 std::cerr << "Error: " << guanaqo::demangled_typename(typeid(e)) << ":\n "
411 << e.what() << std::endl;
412 return -1;
413}
auto get_solver_builder(Options &opts)
std::string get_output_paths(Options &opts)
const auto * docs
std::ostream & get_output_stream(Options &opts, std::ofstream &out_fstream)
void print_version()
auto get_problem_path(const char *const *argv)
void print_usage(const char *a0)
void print_problem_description(std::ostream &os, LoadedProblem &problem, bool show_funcs)
void store_solution(const fs::path &sol_output_dir, std::ostream &os, BenchmarkResults &results, auto &solver, const Options &opts, std::span< const char * > argv)
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[])
std::span< unsigned > used()
Definition options.hpp:61
std::span< const std::string_view > options() const
Definition options.hpp:58
length_t get_num_constraints() const
[Required] Number of constraints.
length_t get_num_variables() const
[Required] Number of decision variables.
#define USING_ALPAQA_CONFIG(Conf)
Definition config.hpp:77
SharedSolverWrapper make_fista_driver(std::string_view direction, Options &opts)
void print_provided_functions(std::ostream &os, const TypeErasedProblem< Conf > &problem)
SharedSolverWrapper make_ipopt_driver(std::string_view, Options &)
SharedSolverWrapper make_lbfgsb_driver(std::string_view, Options &)
auto split_key(std::string_view full, char tok='.')
Split the string full on the first occurrence of tok.
Definition params.hpp:32
auto as_span(Eigen::DenseBase< Derived > &v)
Convert an Eigen vector view to a std::span.
Definition span.hpp:21
EigenConfigd DefaultConfig
Definition config.hpp:31
KKTError< Conf > compute_kkt_error(const TypeErasedProblem< Conf > &problem, crvec< Conf > x, crvec< Conf > y)
Definition kkt-error.hpp:17
void set_params(T &t, std::string_view prefix, Options &opts)
Definition options.hpp:126
SharedSolverWrapper make_zerofpr_driver(std::string_view direction, Options &opts)
SharedSolverWrapper make_panoc_driver(std::string_view direction, Options &opts)
SharedSolverWrapper make_pantr_driver(std::string_view direction, Options &opts)
void print_completion(std::string_view method, std::string_view params)
LoadedProblem load_problem(std::string_view type, const fs::path &dir, const fs::path &file, Options &opts)
Definition problem.cpp:197
std::optional< length_t > nnz_hess_L
Definition problem.hpp:36
std::optional< ConstrCount > general_constr_count
Definition problem.hpp:35
std::optional< length_t > nnz_hess_ψ
Definition problem.hpp:37
alpaqa::TypeErasedProblem< config_t > problem
Definition problem.hpp:25
std::string name
Definition problem.hpp:28
fs::path path
Definition problem.hpp:27
std::optional< ConstrCount > box_constr_count
Definition problem.hpp:34
std::optional< length_t > nnz_jac_g
Definition problem.hpp:36
SharedSolverWrapper make_qpalm_driver(std::string_view, Options &)
std::string random_hex_string(auto &&rng)
Definition results.hpp:53
auto timestamp_ms()
Definition results.hpp:62
void print_results(std::ostream &os, const BenchmarkResults &results)
Definition results.hpp:138
LoadedProblem & problem
Definition results.hpp:45
int64_t timestamp
Definition results.hpp:50
SolverResults solver_results
Definition results.hpp:46