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 <alpaqa/util/print.hpp>
11#include <alpaqa/util/span.hpp>
12#include <guanaqo/demangled-typename.hpp>
13#include <guanaqo/string-util.hpp>
14#include <alpaqa-version.h>
15
17
18#include "fista-driver.hpp"
19#include "ipopt-driver.hpp"
20#include "lbfgsb-driver.hpp"
21#include "panoc-driver.hpp"
22#include "pantr-driver.hpp"
23#include "qpalm-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 <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
45namespace alpaqa::driver {
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, convex-newton.
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 show_funcs: Print an overview of the functions provided by the problem.
100 dl_flags: Flags passed to dlopen when loading 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, std::ostream &os) {
133 const auto *opts = " [<problem-type>:][<path>/]<name> [method=<solver>] "
134 "[<key>=<value>...]\n";
135 os << "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 os << "Third-party libraries:\n"
144 << " * Eigen " << EIGEN_WORLD_VERSION << '.' << EIGEN_MAJOR_VERSION
145 << '.' << 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 << '.' << NLOHMANN_JSON_VERSION_PATCH
174 << " (https://github.com/nlohmann/json) - MIT\n"
175#endif
176 << std::endl;
177}
178
179void print_version(std::ostream &os) {
180 os << ALPAQA_VERSION_FULL " (" << ALPAQA_BUILD_TIME << ")\n";
181}
182
184 : default_solver(std::move(default_solver)),
185 solvers{
186 {"panoc", make_panoc_driver}, {"zerofpr", make_zerofpr_driver},
187 {"pantr", make_pantr_driver}, {"lbfgsb", make_lbfgsb_driver},
188 {"fista", make_fista_driver}, {"ipopt", make_ipopt_driver},
189 {"qpalm", make_qpalm_driver},
190 } {}
191
192std::tuple<solver_builder_func, std::string>
194 std::string method = default_solver, direction;
195 set_params(method, "method", opts);
196 std::tie(method, direction) = alpaqa::params::split_key(method, '.');
197 // Find the selected solver builder
198 auto solver_it = solvers.find(method);
199 if (solver_it == solvers.end())
200 throw std::invalid_argument(
201 "Unknown solver '" + std::string(method) + "'\n" +
202 " Available solvers: " + guanaqo::join(std::views::keys(solvers)));
203 return std::make_tuple(solver_it->second, direction);
204}
205
207 solver_builder_func builder) {
208 solvers.insert({name, std::move(builder)});
209}
210
211/// Split the string @p s on the first occurrence of @p tok.
212/// Returns ("", s) if tok was not found.
213static auto split_once(std::string_view s, char tok = '.') {
214 auto tok_pos = s.find(tok);
215 if (tok_pos == s.npos)
216 return std::make_tuple(std::string_view{}, s);
217 std::string_view key{s.begin(), s.begin() + tok_pos};
218 std::string_view rem{s.begin() + tok_pos + 1, s.end()};
219 return std::make_tuple(key, rem);
220}
221
223 std::ofstream &out_fstream,
224 std::ostream &default_stream) {
225 std::string out_path = "-";
226 set_params(out_path, "out", opts);
227 if (out_path != "-")
228 if (out_fstream.open(out_path); !out_fstream)
229 throw std::runtime_error("Unable to open '" + out_path + "'");
230 return out_fstream.is_open() ? out_fstream : default_stream;
231}
232
234 std::string sol_path;
235 set_params(sol_path, "sol", opts);
236 return sol_path;
237}
238
239std::tuple<fs::path, std::string_view>
240get_problem_path(const char *const *argv) {
241 bool rel_to_exe = argv[1][0] == '^';
242 std::string_view prob_path_s = argv[1] + static_cast<ptrdiff_t>(rel_to_exe);
243 std::string_view prob_type;
244 std::tie(prob_type, prob_path_s) = split_once(prob_path_s, ':');
245 fs::path prob_path{prob_path_s};
246 if (rel_to_exe)
247 prob_path = fs::canonical(fs::path(argv[0])).parent_path() / prob_path;
248 return std::make_tuple(std::move(prob_path), prob_type);
249}
250
251std::string store_solution(const fs::path &sol_output_dir, std::ostream &os,
252 BenchmarkResults &results,
253 const SolverWrapper &solver,
254 [[maybe_unused]] const Options &opts,
255 std::span<const char *const> argv) {
256 const auto &sol_res = results.solver_results;
257 auto timestamp_str = std::to_string(results.timestamp);
258 auto rnd_str = random_hex_string(std::random_device());
259 auto name = results.problem.path.stem().string();
260 if (name == "PROBLEM")
261 name = results.problem.name;
262 auto suffix = '_' + name + '_' + timestamp_str + '_' + rnd_str;
263 fs::create_directories(sol_output_dir);
264 std::array solutions{
265 std::tuple{"solution", "x", &sol_res.solution},
266 std::tuple{"multipliers for g", "mul_g", &sol_res.multipliers},
267 std::tuple{"multipliers for x", "mul_x", &sol_res.multipliers_bounds},
268 };
269 std::string warm_start_cmd;
270 for (auto [name, fname, value] : solutions) {
271 if (value->size() == 0)
272 continue;
273 auto pth = sol_output_dir / (std::string(fname) + suffix + ".csv");
274 os << "Writing " << name << " to " << pth << std::endl;
275 std::ofstream output_file(pth);
276 guanaqo::print_csv(output_file, alpaqa::as_span(*value));
277 if (!warm_start_cmd.empty())
278 warm_start_cmd += " ";
279 warm_start_cmd += "'";
280 warm_start_cmd += fname;
281 warm_start_cmd += "0=@";
282 warm_start_cmd += pth.string();
283 warm_start_cmd += "'";
284 }
285 {
286 auto pth = sol_output_dir / ("cmdline" + suffix + ".txt");
287 os << "Writing arguments to " << pth << std::endl;
288 std::ofstream output_file(pth);
289 for (const char *arg : argv)
290 output_file << std::quoted(arg, '\'') << ' ';
291 output_file << '\n';
292 }
293 if (solver.has_statistics()) {
294 auto pth = sol_output_dir / ("stats" + suffix + ".csv");
295 os << "Writing statistics to " << pth << std::endl;
296 std::ofstream output_file(pth);
297 solver.write_statistics_to_stream(output_file);
298 }
299#if ALPAQA_WITH_JSON
300 {
301 auto pth = sol_output_dir / ("options" + suffix + ".json");
302 os << "Writing options to " << pth << std::endl;
303 std::ofstream output_file(pth);
304 output_file << opts.get_json_out() << '\n';
305 }
306#endif
307 {
308 auto pth = sol_output_dir / ("eval" + suffix + ".yml");
309 os << "Writing evaluation counters to " << pth << std::endl;
310 std::ofstream output_file(pth);
311 write_evaluations(output_file, sol_res.evals);
312 }
313 if (!warm_start_cmd.empty()) {
314 os << "To warm start using this solution, use: " << warm_start_cmd
315 << std::endl;
316 }
317 return suffix;
318}
319
320} // namespace alpaqa::driver
#define USING_ALPAQA_CONFIG(Conf)
Definition config.hpp:77
auto get_problem_path(const char *const *argv)
auto split_once(std::string_view s, char tok='.')
Split the string s on the first occurrence of tok.
SharedSolverWrapper make_zerofpr_driver(std::string_view direction, alpaqa::Options &opts)
SharedSolverWrapper make_fista_driver(std::string_view direction, alpaqa::Options &opts)
SharedSolverWrapper make_qpalm_driver(std::string_view, alpaqa::Options &)
const auto * docs
void write_evaluations(std::ostream &os, const EvalCounter &evals)
Definition results.hpp:68
ALPAQA_DRIVERS_EXPORT std::string store_solution(const fs::path &sol_output_dir, std::ostream &os, BenchmarkResults &results, const SolverWrapper &solver, const alpaqa::Options &opts, std::span< const char *const > argv)
SharedSolverWrapper make_lbfgsb_driver(std::string_view, alpaqa::Options &)
ALPAQA_DRIVERS_EXPORT std::string get_output_paths(alpaqa::Options &opts)
ALPAQA_DRIVERS_EXPORT std::ostream & get_output_stream(alpaqa::Options &opts, std::ofstream &out_fstream, std::ostream &default_stream)
SharedSolverWrapper make_ipopt_driver(std::string_view, Options &)
std::string random_hex_string(auto &&rng)
Definition results.hpp:52
SharedSolverWrapper make_panoc_driver(std::string_view direction, alpaqa::Options &opts)
ALPAQA_DRIVERS_EXPORT void print_version(std::ostream &os)
std::function< SharedSolverWrapper(std::string_view, alpaqa::Options &)> solver_builder_func
ALPAQA_DRIVERS_EXPORT void print_usage(const char *a0, std::ostream &os)
SharedSolverWrapper make_pantr_driver(std::string_view direction, alpaqa::Options &opts)
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
void set_params(T &t, std::string_view prefix, Options &opts)
Definition options.hpp:128
std::tuple< solver_builder_func, std::string > get_solver_builder(alpaqa::Options &opts) const
void register_solver_builder(std::string_view name, solver_builder_func builder)
SolverBuilders(std::string default_solver="panoc")
std::map< std::string_view, solver_builder_func > solvers
virtual void write_statistics_to_stream(std::ostream &) const
virtual bool has_statistics() const