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