alpaqa 1.0.0a19
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
102 The prefix @ can be added to the values of x0, mul_g0 and mul_x0 to read
103 the values from the given CSV file.
104
105 Options can be loaded from a JSON file by using an @ prefix. For example,
106 an argument @options.json loads the options from a file options.json in the
107 current directory. Multiple JSON files are processed in the order they
108 appear in the command line arguments. Options specified on the command line
109 always have precedence over options in a JSON file, regardless of order.
110
111examples:
112 alpaqa-driver problem.so \
113 problem.register=register_alpaqa_problem \
114 problem.custom_arg=foo \
115 method=panoc.struclbfgs \
116 accel.memory=50 \
117 alm.{tolerance,dual_tolerance}=1e-8 \
118 solver.print_interval=50 \
119 x0=@/some/file.csv
120
121 alpaqa-driver cs:build/casadi_problem.so \
122 @options/default.json \
123 problem.param=1,2,3 \
124 method=ipopt \
125 solver.tol=1e-8 solver.constr_viol_tol=1e-8 \
126 solver.warm_start_init_point=yes \
127 x0=@/some/file.csv \
128 mul_g0=@/some/other/file.csv \
129 mul_x0=@/yet/another/file.csv
130)==";
131
132void print_usage(const char *a0) {
133 const auto *opts = " [<problem-type>:][<path>/]<name> [method=<solver>] "
134 "[<key>=<value>...]\n";
135 std::cout << "alpaqa-driver " ALPAQA_VERSION_FULL " (" << alpaqa_build_time
136 << ")\n\n"
137 " Command-line interface to the alpaqa solvers.\n"
138 " alpaqa is published under the LGPL-3.0.\n"
139 " https://github.com/kul-optec/alpaqa"
140 "\n\n"
141 " Usage: "
142 << a0 << opts << docs << std::endl;
143 std::cout << "Third-party libraries:\n"
144 << " * Eigen " << EIGEN_WORLD_VERSION << '.'
145 << EIGEN_MAJOR_VERSION << '.' << EIGEN_MINOR_VERSION
146 << " (https://gitlab.com/libeigen/eigen) - MPL-2.0\n"
147#ifdef ALPAQA_WITH_EXTERNAL_CASADI
148 << " * CasADi " CASADI_VERSION_STRING
149 " (https://github.com/casadi/casadi) - LGPL-3.0-or-later\n"
150#endif
151#ifdef ALPAQA_WITH_CUTEST
152 << " * CUTEst"
153 " (https://github.com/ralna/CUTEst) - BSD-3-Clause\n"
154#endif
155#ifdef ALPAQA_WITH_LBFGSB
156 << " * L-BFGS-B 3.0 "
157 "(http://users.iems.northwestern.edu/~nocedal/lbfgsb.html) - "
158 "BSD-3-Clause\n"
159#endif
160#ifdef ALPAQA_WITH_IPOPT
161 << " * Ipopt " IPOPT_VERSION
162 " (https://github.com/coin-or/Ipopt) - EPL-2.0\n"
163 << " * MUMPS (https://mumps-solver.org) - CECILL-C\n"
164 << " * OpenBLAS (https://github.com/OpenMathLib/OpenBLAS) - "
165 "BSD-3-Clause\n"
166#endif
167#ifdef ALPAQA_WITH_QPALM
168 << " * QPALM " QPALM_VERSION_STR
169 " (https://github.com/kul-optec/QPALM) - LGPL-3.0\n"
170#endif
171#ifdef ALPAQA_WITH_JSON
172 << " * nlohmann/json " << NLOHMANN_JSON_VERSION_MAJOR << '.'
173 << NLOHMANN_JSON_VERSION_MINOR << '.'
174 << NLOHMANN_JSON_VERSION_PATCH
175 << " (https://github.com/nlohmann/json) - MIT\n"
176#endif
177 << std::endl;
178}
179
181 std::cout << ALPAQA_VERSION_FULL " (" << ALPAQA_BUILD_TIME << ")\n";
182}
183
184/// Split the string @p s on the first occurrence of @p tok.
185/// Returns ("", s) if tok was not found.
186auto split_once(std::string_view s, char tok = '.') {
187 auto tok_pos = s.find(tok);
188 if (tok_pos == s.npos)
189 return std::make_tuple(std::string_view{}, s);
190 std::string_view key{s.begin(), s.begin() + tok_pos};
191 std::string_view rem{s.begin() + tok_pos + 1, s.end()};
192 return std::make_tuple(key, rem);
193}
194
195std::ostream &get_output_stream(Options &opts, std::ofstream &out_fstream) {
196 std::string out_path = "-";
197 set_params(out_path, "out", opts);
198 if (out_path != "-")
199 if (out_fstream.open(out_path); !out_fstream)
200 throw std::runtime_error("Unable to open '" + out_path + "'");
201 return out_fstream.is_open() ? out_fstream : std::cout;
202}
203
204std::string get_output_paths(Options &opts) {
205 std::string sol_path;
206 set_params(sol_path, "sol", opts);
207 return sol_path;
208}
209
210auto get_problem_path(const char *const *argv) {
211 bool rel_to_exe = argv[1][0] == '^';
212 std::string_view prob_path_s = argv[1] + static_cast<ptrdiff_t>(rel_to_exe);
213 std::string_view prob_type;
214 std::tie(prob_type, prob_path_s) = split_once(prob_path_s, ':');
215 fs::path prob_path{prob_path_s};
216 if (rel_to_exe)
217 prob_path = fs::canonical(fs::path(argv[0])).parent_path() / prob_path;
218 return std::make_tuple(std::move(prob_path), prob_type);
219}
220
221void print_problem_description(std::ostream &os, LoadedProblem &problem,
222 bool show_funcs) {
223 os << "Loaded problem \"" << problem.name << "\"\n"
224 << "Number of variables: " << problem.problem.get_n() << "\n"
225 << "Number of constraints: " << problem.problem.get_m() << "\n";
226 if (problem.nnz_jac_g)
227 os << "Nonzeros in Jg: " << *problem.nnz_jac_g << "\n";
228 if (problem.nnz_hess_L)
229 os << "Nonzeros in ∇²L: " << *problem.nnz_hess_L << "\n";
230 if (problem.nnz_hess_ψ)
231 os << "Nonzeros in ∇²ψ: " << *problem.nnz_hess_ψ << "\n";
232 if (problem.box_constr_count)
233 os << "Box constraints:"
234 << "\n Fixed variables: " << problem.box_constr_count->eq
235 << "\n Bilateral: " << problem.box_constr_count->lbub
236 << "\n Lower bound only: " << problem.box_constr_count->lb
237 << "\n Upper bound only: " << problem.box_constr_count->ub
238 << "\n";
239 if (problem.general_constr_count)
240 os << "General constraints:"
241 << "\n Equality: " << problem.general_constr_count->eq
242 << "\n Bilateral: " << problem.general_constr_count->lbub
243 << "\n Lower bound only: " << problem.general_constr_count->lb
244 << "\n Upper bound only: " << problem.general_constr_count->ub
245 << "\n";
246 if (show_funcs) {
247 os << "Provided functions:\n";
249 }
250}
251
253 std::string method = "panoc", direction;
254 set_params(method, "method", opts);
255 std::tie(method, direction) = alpaqa::params::split_key(method, '.');
256 // Dictionary of available solver builders
257 std::map<std::string_view, solver_builder_func> solvers{
258 {"panoc", make_panoc_driver}, {"zerofpr", make_zerofpr_driver},
259 {"pantr", make_pantr_driver}, {"lbfgsb", make_lbfgsb_driver},
260 {"fista", make_fista_driver}, {"ipopt", make_ipopt_driver},
261 {"qpalm", make_qpalm_driver},
262 };
263 // Find the selected solver builder
264 auto solver_it = solvers.find(method);
265 if (solver_it == solvers.end())
266 throw std::invalid_argument(
267 "Unknown solver '" + std::string(method) + "'\n" +
268 " Available solvers: " +
269 alpaqa::util::join(std::views::keys(solvers)));
270 ;
271 return std::make_tuple(std::move(solver_it->second), direction);
272}
273
274void store_solution(const fs::path &sol_output_dir, std::ostream &os,
275 BenchmarkResults &results, auto &solver,
276 [[maybe_unused]] const Options &opts,
277 std::span<const char *> argv) {
278 const auto &sol_res = results.solver_results;
279 auto timestamp_str = std::to_string(results.timestamp);
280 auto rnd_str = random_hex_string(std::random_device());
281 auto name = results.problem.path.stem().string();
282 if (name == "PROBLEM")
283 name = results.problem.name;
284 auto suffix = '_' + name + '_' + timestamp_str + '_' + rnd_str;
285 fs::create_directories(sol_output_dir);
286 std::array solutions{
287 std::tuple{"solution", "sol_x", &sol_res.solution},
288 std::tuple{"multipliers for g", "mul_g", &sol_res.multipliers},
289 std::tuple{"multipliers for x", "mul_x", &sol_res.multipliers_bounds},
290 };
291 for (auto [name, fname, value] : solutions) {
292 if (value->size() == 0)
293 continue;
294 auto pth = sol_output_dir / (std::string(fname) + suffix + ".csv");
295 os << "Writing " << name << " to " << pth << std::endl;
296 std::ofstream output_file(pth);
297 alpaqa::print_csv(output_file, *value);
298 }
299 {
300 auto pth = sol_output_dir / ("cmdline" + suffix + ".txt");
301 os << "Writing arguments to " << pth << std::endl;
302 std::ofstream output_file(pth);
303 for (const char *arg : argv)
304 output_file << std::quoted(arg, '\'') << ' ';
305 output_file << '\n';
306 }
307 if (solver->has_statistics()) {
308 auto pth = sol_output_dir / ("stats" + suffix + ".csv");
309 os << "Writing statistics to " << pth << std::endl;
310 std::ofstream output_file(pth);
311 solver->write_statistics_to_stream(output_file);
312 }
313#if ALPAQA_WITH_JSON
314 {
315 auto pth = sol_output_dir / ("options" + suffix + ".json");
316 os << "Writing options to " << pth << std::endl;
317 std::ofstream output_file(pth);
318 output_file << opts.get_json_out() << '\n';
319 }
320#endif
321}
322
323int main(int argc, const char *argv[]) try {
324#ifdef _WIN32
325 SetConsoleOutputCP(CP_UTF8);
326#endif
327 // Check command line options
328 if (argc < 1)
329 return -1;
330 if (argc == 1)
331 return print_usage(argv[0]), 0;
332 if (argc < 2)
333 return print_usage(argv[0]), -1;
334 if (argv[1] == "-h"sv || argv[1] == "--help"sv || argv[1] == "?"sv)
335 return print_usage(argv[0]), 0;
336 if (argv[1] == "-v"sv || argv[1] == "--version"sv)
337 return print_version(), 0;
338 if (argv[1] == "--complete"sv) {
339 if (argc < 4)
340 return -1;
341 print_completion(argv[2], argv[3]);
342 return 0;
343 }
344
345 std::span args{argv, static_cast<size_t>(argc)};
346 Options opts{argc - 2, argv + 2};
347
348 // Check where to write the output to
349 std::ofstream out_fstream;
350 std::ostream &os = get_output_stream(opts, out_fstream);
351
352 // Check which problem to load
353 auto [prob_path, prob_type] = get_problem_path(argv);
354
355 // Check which solver to use
356 auto [solver_builder, direction] = get_solver_builder(opts);
357
358 // Check output paths
359 fs::path sol_output_dir = get_output_paths(opts);
360
361 // Build solver
362 auto solver = solver_builder(direction, opts);
363
364 // Load problem
365 os << "Loading " << prob_path << " ..." << std::endl;
366 auto problem = load_problem(prob_type, prob_path.parent_path(),
367 prob_path.filename(), opts);
368 // Print problem information
369 bool show_funcs = false;
370 set_params(show_funcs, "show_funcs", opts);
371 print_problem_description(os, problem, show_funcs);
372 os << std::endl;
373
374 // Check options
375 auto used = opts.used();
376 auto unused_opt = std::ranges::find(used, 0);
377 auto unused_idx = static_cast<size_t>(unused_opt - used.begin());
378 if (unused_opt != used.end())
379 throw std::invalid_argument("Unused option: " +
380 std::string(opts.options()[unused_idx]));
381
382 // Solve
383 auto solver_results = solver->run(problem, os);
384
385 // Compute more statistics
386 real_t f = problem.problem.eval_f(solver_results.solution);
387 auto kkt_err = alpaqa::compute_kkt_error(
388 problem.problem, solver_results.solution, solver_results.multipliers);
389 BenchmarkResults results{
390 .problem = problem,
391 .solver_results = solver_results,
392 .objective = f + solver_results.h,
393 .smooth_objective = f,
394 .error = kkt_err,
395 .options = opts.options(),
396 .timestamp = timestamp_ms<std::chrono::system_clock>().count(),
397 };
398
399 // Print results
400 print_results(os, results);
401
402 // Store solution
403 if (!sol_output_dir.empty())
404 store_solution(sol_output_dir, os, results, solver, opts, args);
405
406} catch (std::exception &e) {
407 std::cerr << "Error: " << demangled_typename(typeid(e)) << ":\n "
408 << e.what() << std::endl;
409 return -1;
410}
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:128
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:183
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:174