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