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