proxsuite 0.7.2 The Advanced Proximal Optimization Toolbox |
ProxQP solves convex quadratic programs, which minimize a convex quadratic cost under some linear constraints. It is mathematically described as:
H is a real symmetric positive semi-definite matrix. d is the problem dimension (i.e., the number of primal variables), while n_eq and n_in are the numbers of equality and inequality constraints, respectively.
For linearly constrained convex optimization problems such as \eqref{eq:QP}, strong duality holds, and the associated KKT conditions are necessary and sufficient for ensuring a primal-dual point (x,y,z) to be optimal (see, e.g.,Section 5.2.3} and Section 2, page 5 for more details). For \eqref{eq:QP}, the KKT system is given by the set of equations:
where the last equation involves the Hadamard product (i.e., for two vectors, u and v, the Hadamard product is the vector whose ith entry is u_i v_i).
In practice, we look for a triplet (x,y,z) satisfying these optimality conditions \eqref{qp:kkt} up to a certain level of absolute accuracy (dependent of the application), leading us to the following absolute stopping criterion on the primal and dual residuals:
The infinite norm is preferred to the L2 norm as it is independent of the problem dimensions. It is also common to consider relative convergence criteria for early stopping, as absolute targets might not be reached due to numerical issues. ProxQP provides it in a similar way as OSQP (for more details, see, e.g., OSQP's convergence criteria or section 3.4 in the corresponding paper). Hence more generally, the following stopping criterion can be used:
It is important to note that this stopping criterion on primal and dual residuals is not enough to guarantee that the returned solution satisfies all \eqref{qp:kkt} conditions. Indeed, as the problem has affine constraints and the objective is quadratic and convex, then as soon as the primal or the dual problem is feasible, strong duality holds (see, e.g., Theorem 2 from L. El Ghaoui's lesson) and to satisfy all optimality conditions we need to add a third criterion on the duality gap
where
Finally, note that ProxQP has a specific feature for handling primal infeasibility. More precisely, if the problem appears to be primal infeasible, it will solve the closest primal feasible problem in
You can find more details on these subjects (and how to activate this feature with ProxQP) in the subsections describing the Settings and Results classes. You can also find more technical references with this work.
ProxQP algorithm is implemented in two versions specialized for dense and sparse matrices. One simple and unified API has been designed for loading dense and sparse backends. Concretely, it contains three methods:
In what follows, we will make several examples to illustrate how to use this API in C++ and Python. Some subtle differences exist, nevertheless, between the dense and sparse backends, and we will point them out when needed. We will also present all solver's possible settings and show where the results are stored. We will then give some recommendations about which backend to use, considering your needs.
When creating a Qp object in C++ or Python, it automatically contains the following sub-classes:
For loading ProxQP with the dense backend, it is as simple as the following code below:
| examples/cpp/loading_dense_qp.cpp | examples/python/loading_dense_qp.py |
|---|---|
#include <iostream> #include "proxsuite/proxqp/dense/dense.hpp" using namespace proxsuite::proxqp; using T = double; int main() { dense::isize dim = 10; dense::isize n_eq(dim / 4); dense::isize n_in(dim / 4); dense::QP<T> qp(dim, n_eq, n_in); } _detail::_meta::make_signed< usize >::Type isize Definition typedefs.hpp:43 Definition backward_data.hpp:16 Definition wrapper.hpp:116 |
The dimensions of the problem (i.e., n is the dimension of primal variable x, n_eq the number of equality constraints, and n_in the number of inequality constraints) are used for allocating the space needed for the Qp object. The dense Qp object is templated by the floating precision of the QP model (in the example above in C++ a double precision). Note that for the model to be valid, the primal dimension (i.e., n) must be strictly positive. If it is not the case, an assertion will be raised precising this issue.
The dense backend also has a specific feature for efficiently handling box inequality constraints. To benefit from it, constructors are overloaded as follows:
| examples/cpp/loading_dense_qp_with_box_constraints.cpp | examples/python/loading_dense_qp_with_box_constraints.py |
|---|---|
Furthermore, the dense version of ProxQP has two different backends with different advantages:
The QP constructor uses, by default, an automatic choice for deciding which backend suits a priori bests user's needs. It is based on a heuristic comparing a priori computational complexity of each backend. However, if you have more insights into your needs (e.g., accuracy specifications, primal dimension is known to be far larger than the one of the constraints etc.), we encourage you to specify directly in the constructor which backend to use. It is as simple as the following:
| examples/cpp/loading_dense_qp_with_different_backend_choice.cpp | examples/python/loading_dense_qp_with_different_backend_choice.py |
|---|---|
#include <iostream> #include "proxsuite/proxqp/dense/dense.hpp" using namespace proxsuite::proxqp; using T = double; int main() { dense::isize dim = 10; dense::isize n_eq(dim / 4); dense::isize n_in(dim / 4); // we load here a QP model with // n_eq equality constraints // n_in generic type of inequality constraints // and dim box inequality constraints // we specify PrimalDualLDLT backend dense::QP<T> qp( dim, n_eq, n_in, true, proxsuite::proxqp::DenseBackend::PrimalDualLDLT); // true specifies we take into accounts box constraints // n_in are any other type of inequality constraints // Other examples // we load here a QP model with // n_eq equality constraints // O generic type of inequality constraints // and dim box inequality constraints // we specify PrimalLDLT backend dense::QP<T> qp2( dim, n_eq, 0, true, proxsuite::proxqp::DenseBackend::PrimalLDLT); // true specifies we take into accounts box constraints // we don't need to precise n_in = dim, it is taken // into account internally // we let here the solver decide with Automatic // backend choice. dense::QP<T> qp3( dim, n_eq, 0, true, proxsuite::proxqp::DenseBackend::Automatic); // Note that it is the default choice } | import proxsuite n = 10 n_eq = 2 n_in = 2 # we load here a QP model with # n_eq equality constraints # n_in generic type of inequality constraints # and dim box inequality constraints # we specify PrimalDualLDLT backend n, n_eq, n_in, True, dense_backend=proxsuite.proxqp.dense.DenseBackend.PrimalDualLDLT, ) # true specifies we take into accounts box constraints # n_in are any other type of inequality constraints # Other examples # we load here a QP model with # n_eq equality constraints # O generic type of inequality constraints # and dim box inequality constraints # we specify PrimalLDLT backend qp2 = proxsuite.proxqp.dense.QP( n, n_eq, 0, True, dense_backend=proxsuite.proxqp.dense.DenseBackend.PrimalLDLT ) # true specifies we take into accounts box constraints # we don't need to precise n_in = dim, it is taken # into account internally # We let finally the solver decide qp3 = proxsuite.proxqp.dense.QP( n, n_eq, 0, True, dense_backend=proxsuite.proxqp.dense.DenseBackend.Automatic ) # Note that it is the default choice |
For loading ProxQP with the sparse backend, they are two possibilities:
| examples/cpp/loading_sparse_qp.cpp | examples/python/loading_sparse_qp.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/sparse/sparse.hpp> // get the sparse API of ProxQP #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite::proxqp; using T = double; int main() { // design a qp object using QP problem dimensions isize n = 10; isize n_eq(n / 4); isize n_in(n / 4); sparse::QP<T, isize> qp(n, n_eq, n_in); // assume you generate these matrices H, A and C for your QP problem T p = 0.15; // level of sparsity T conditioning = 10.0; // conditioning level for H n, conditioning, p); auto A = ::proxsuite::proxqp::utils::rand::sparse_matrix_rand<T>(n_eq, n, p); auto C = ::proxsuite::proxqp::utils::rand::sparse_matrix_rand<T>(n_in, n, p); // design a qp2 object using sparsity masks of H, A and C H.cast<bool>(), A.cast<bool>(), C.cast<bool>()); } auto sparse_positive_definite_rand(isize n, Scalar cond, Scalar p) -> SparseMat< Scalar > Definition random_qp_problems.hpp:229 auto sparse_matrix_rand(isize nrows, isize ncols, Scalar p) -> SparseMat< Scalar > Definition random_qp_problems.hpp:338 This class defines the API of PROXQP solver with sparse backend. Definition wrapper.hpp:91 | import proxsuite from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.sparse.QP(n, n_eq, n_in) # load a qp2 object using matrix masks H, g, A, b, C, u, l = generate_mixed_qp(n, True) H_ = H != 0.0 A_ = A != 0.0 C_ = C != 0.0 qp2 = proxsuite.proxqp.sparse.QP(H_, A_, C_) |
The sparse Qp object is templated by the floating precision of the QP model (in the example above, a double precision) and the integer precision used for the different types of non-zero indices used (for the associated sparse matrix representation used).
Once you have defined a Qp object, the init method enables you to set up the QP problem to be solved (the example is given for the dense backend, it is similar for the sparse backend).
| examples/cpp/init_dense_qp.cpp | examples/python/init_dense_qp.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/dense/dense.hpp> // load the dense solver backend #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite::proxqp; using T = double; int main() { isize dim = 10; isize n_eq(dim / 4); isize n_in(dim / 4); // generate a random qp T sparsity_factor(0.15); T strong_convexity_factor(1.e-2); dense::Model<T> qp_random = utils::dense_strongly_convex_qp( dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); dense::QP<T> qp(dim, n_eq, n_in); // create the QP object qp.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u); // initialize the model } proxsuite::proxqp::dense::Model< Scalar > dense_strongly_convex_qp(proxqp::isize dim, proxqp::isize n_eq, proxqp::isize n_in, Scalar sparsity_factor, Scalar strong_convexity_factor=Scalar(1e-2)) Definition random_qp_problems.hpp:464 This class stores the model of the QP problem. Definition model.hpp:24 | import proxsuite from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.dense.QP(n, n_eq, n_in) # generate a random QP H, g, A, b, C, u, l = generate_mixed_qp(n) # initialize the model of the problem to solve qp.init(H, g, A, b, C, l, u) |
If you use the specific feature of the dense backend for handling box constraints, the init method uses simply as follows:
| examples/cpp/init_dense_qp_with_box.cpp | examples/python/init_dense_qp_with_box.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/dense/dense.hpp> // load the dense solver backend #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite::proxqp; using T = double; int main() { isize dim = 10; isize n_eq(dim / 4); isize n_in(dim / 4); // generate a random qp T sparsity_factor(0.15); T strong_convexity_factor(1.e-2); dense::Model<T> qp_random = utils::dense_strongly_convex_qp( dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); // specify some trivial box constraints dense::Vec<T> u_box(dim); dense::Vec<T> l_box(dim); u_box.setZero(); l_box.setZero(); u_box.array() += 1.E10; l_box.array() -= 1.E10; // and specify with true you take into account box constraints // in the model // you can check that there are constraints with method is_box_constrained std::cout << "the qp is box constrained : " << qp.is_box_constrained() << std::endl; // init the model qp.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u, l_box, u_box); // initialize the model } | import proxsuite import numpy as np from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.dense.QP(n, n_eq, n_in, True) # you can check that there are constraints with method is_box_constrained print(f"the qp is box constrained: {qp.is_box_constrained()}") # generate a random QP H, g, A, b, C, u, l = generate_mixed_qp(n) l_box = -np.ones(n) * 1.0e10 u_box = np.ones(n) * 1.0e10 # initialize the model of the problem to solve qp.init(H, g, A, b, C, l, u, l_box, u_box) |
Note that with its dense backend, ProxQP solver manipulates matrices in dense representations (in the same spirit, the solver with sparse backend manipulates entries in sparse format). In the example above, the matrices are originally in sparse format and eventually converted into dense format. Note that if some elements of your QP model are not defined (for example, a QP without linear cost or inequality constraints), you can either pass a None argument or a matrix with zero shapes for specifying it. We provide an example below in cpp and Python (for the dense case, it is similar with sparse backend).
| examples/cpp/initializing_with_none.cpp | examples/python/initializing_with_none.py |
|---|---|
#include <iostream> #include "proxsuite/proxqp/dense/dense.hpp" #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite::proxqp; using T = double; int main() { dense::isize dim = 10; dense::isize n_eq(0); dense::isize n_in(0); dense::QP<T> qp(dim, n_eq, n_in); T strong_convexity_factor(0.1); T sparsity_factor(0.15); // we generate a qp, so the function used from helpers.hpp is // in proxqp namespace. The qp is in dense eigen format and // you can control its sparsity ratio and strong convexity factor. dense::Model<T> qp_random = utils::dense_strongly_convex_qp( dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); qp.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u); // initialization with zero shape matrices // it is equivalent to do qp.init(qp_random.H, qp_random.g, // nullopt,nullopt,nullopt,nullopt,nullopt); qp.solve(); // print an optimal solution x,y and z std::cout << "optimal x: " << qp.results.x << std::endl; std::cout << "optimal y: " << qp.results.y << std::endl; std::cout << "optimal z: " << qp.results.z << std::endl; } | import numpy as np import proxsuite H = np.array([[65.0, -22.0, -16.0], [-22.0, 14.0, 7.0], [-16.0, 7.0, 5.0]]) g = np.array([-13.0, 15.0, 7.0]) A = None b = None C = None u = None l = None qp = proxsuite.proxqp.dense.QP(3, 0, 0) qp.init(H, g, A, b, C, l, u) # it is equivalent to do as well qp.init(H, g) qp.solve() print("optimal x: {}".format(qp.results.x)) |
With the init method, you can also setting-up on the same time some other parameters in the following order:
We provide below one example in C++ and Python.
| examples/cpp/init_dense_qp_with_other_options.cpp | examples/python/init_dense_qp_with_other_options.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/dense/dense.hpp> // load the dense solver backend #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite::proxqp; using T = double; int main() { isize dim = 10; isize n_eq(dim / 4); isize n_in(dim / 4); // generate a random qp T sparsity_factor(0.15); T strong_convexity_factor(1.e-2); dense::Model<T> qp_random = utils::dense_strongly_convex_qp( dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); dense::QP<T> qp( dim, n_eq, n_in); // create the QP // initialize the model, along with another rho parameter qp.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u, true, /*rho*/ 1.e-7); // in c++ you must follow the order speficied in the API for the parameters // if you don't want to change one parameter (here compute_preconditioner), // just let it be nullopt } | import proxsuite from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.dense.QP(n, n_eq, n_in) # generate a random QP H, g, A, b, C, u, l = generate_mixed_qp(n) # initialize the model of the problem to solve with another rho parameter qp.init(H, g, A, b, C, l, u, rho=1.0e-7) |
Furthermore, some settings must be defined before the init method takes effect. For example, if you want the solver to compute the runtime properly (the sum of the setup time and the solving time), you must set this option before the init method (which is part of the setup time). We provide below an example.
| examples/cpp/init_dense_qp_with_timings.cpp | examples/python/init_dense_qp_with_timings.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/dense/dense.hpp> // load the dense solver backend #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite::proxqp; using T = double; int main() { isize dim = 10; isize n_eq(dim / 4); isize n_in(dim / 4); // generate a random qp T sparsity_factor(0.15); T strong_convexity_factor(1.e-2); dense::Model<T> qp_random = utils::dense_strongly_convex_qp( dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); dense::QP<T> qp(dim, n_eq, n_in); // create the QP object qp.settings.compute_timings = true; // compute all timings qp.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u); // initialize the model } | import proxsuite from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.dense.QP(n, n_eq, n_in) # generate a random QP H, g, A, b, C, u, l = generate_mixed_qp(n) # initialize the model of the problem to solve qp.settings.compute_timings # compute all timings qp.init(H, g, A, b, C, l, u) |
Once you have defined a Qp object and initialized it with a model, the solve method enables you solving the QP problem. The method is overloaded with two modes considering whether you provide or not a warm start to the method. We give below two examples (for the dense backend, with the sparse one it is similar).
| examples/cpp/solve_dense_qp.cpp | examples/python/solve_dense_qp.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/dense/dense.hpp> // load the dense solver backend #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite::proxqp; using T = double; int main() { isize dim = 10; isize n_eq(dim / 4); isize n_in(dim / 4); // generate a random qp T sparsity_factor(0.15); T strong_convexity_factor(1.e-2); dense::Model<T> qp_random = utils::dense_strongly_convex_qp( dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); dense::QP<T> qp(dim, n_eq, n_in); // create the QP object qp.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u); // initialize the model qp.solve(); // solve the problem without warm start auto x_wm = utils::rand::vector_rand<T>(dim); auto y_wm = utils::rand::vector_rand<T>(n_eq); auto z_wm = utils::rand::vector_rand<T>(n_in); qp.solve(x_wm, y_wm, z_wm); // if you have a warm start, put it here // print an optimal solution x,y and z std::cout << "optimal x: " << qp.results.x << std::endl; std::cout << "optimal y: " << qp.results.y << std::endl; std::cout << "optimal z: " << qp.results.z << std::endl; // Another example if you have box constraints (for the dense backend only for // the moment) // some trivial boxes dense::Vec<T> u_box(dim); dense::Vec<T> l_box(dim); u_box.setZero(); l_box.setZero(); u_box.array() += 1.E10; l_box.array() -= 1.E10; qp2.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u, l_box, u_box); // initialize the model with the boxes qp2.solve(); // solve the problem // An important note regarding the inequality multipliers // auto z_ineq = qp.results.z.head(n_in); contains the multiplier associated // to qp_random.C z_box = qp.results.z.tail(dim); the last dim elements // correspond to multiplier associated to the box constraints } auto vector_rand(isize nrows) -> Vec< Scalar > Definition random_qp_problems.hpp:152 | import proxsuite import numpy as np from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.dense.QP(n, n_eq, n_in) # generate a random QP H, g, A, b, C, u, l = generate_mixed_qp(n) # initialize the model of the problem to solve qp.init(H, g, A, b, C, l, u) # solve without warm start qp.solve() # solve with a warm start, for ex random one qp.solve(np.random.randn(n), np.random.randn(n_eq), np.random.randn(n_in)) # print an optimal solution print("optimal x: {}".format(qp.results.x)) print("optimal y: {}".format(qp.results.y)) print("optimal z: {}".format(qp.results.z)) # Another example if you have box constraints (for the dense backend only for the moment) qp2 = proxsuite.proxqp.dense.QP(n, n_eq, n_in, True) l_box = -np.ones(n) * 1.0e10 u_box = np.ones(n) * 1.0e10 qp2.init(H, g, A, b, C, l, u, l_box, u_box) qp2.solve() # An important note regarding the inequality multipliers z_ineq = qp.results.z[:n_in] # contains the multiplier associated to qp_random.C z_box = qp.results.z[ n_in: ] # the last dim elements correspond to multiplier associated to # the box constraints |
Before ending this section, we will talk about how to activate some other settings before launching the solve method. To do so, you only need to define your desired settings (for example, the stopping criterion accuracy threshold eps_abs, or the verbose option) after initializing the Qp object. They will then be taken into account only if there are set before the solve method (otherwise, they will be taken into account when the next solve or update method is called). The full description of all the settings is provided in a dedicated section below. Here we just give an example to illustrate the mentioned notion above.
| examples/cpp/solve_dense_qp_with_setting.cpp | examples/python/solve_dense_qp_with_setting.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/dense/dense.hpp> // load the dense solver backend #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite::proxqp; using T = double; int main() { dense::isize dim = 10; dense::isize n_eq(dim / 4); dense::isize n_in(dim / 4); // generate a random qp T sparsity_factor(0.15); T strong_convexity_factor(1.e-2); dense::Model<T> qp_random = utils::dense_strongly_convex_qp( dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); dense::QP<T> qp(dim, n_eq, n_in); // create the QP object qp.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u); // initialize the model qp.settings.eps_abs = T(1.E-9); // set accuracy threshold to 1.e-9 qp.settings.verbose = true; // print some intermediary results qp.solve(); // solve the problem with previous settings // print an optimal solution x,y and z std::cout << "optimal x: " << qp.results.x << std::endl; std::cout << "optimal y: " << qp.results.y << std::endl; std::cout << "optimal z: " << qp.results.z << std::endl; } | import proxsuite from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.dense.QP(n, n_eq, n_in) # generate a random QP H, g, A, b, C, u, l = generate_mixed_qp(n) # initialize the model of the problem to solve qp.init(H, g, A, b, C, l, u) qp.settings.eps_abs = 1.0e-9 qp.settings.verbose = True # solve with previous settings qp.solve() # print an optimal solution print("optimal x: {}".format(qp.results.x)) print("optimal y: {}".format(qp.results.y)) print("optimal z: {}".format(qp.results.z)) |
The update method is used to update the model or a parameter of the problem, as for the init method. We provide below an example for the dense case.
| examples/cpp/update_dense_qp.cpp | examples/python/update_dense_qp.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/dense/dense.hpp> // load the dense solver backend #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite::proxqp; using T = double; int main() { isize dim = 10; isize n_eq(dim / 4); isize n_in(dim / 4); // generate a random qp T sparsity_factor(0.15); T strong_convexity_factor(1.e-2); dense::Model<T> qp_random = utils::dense_strongly_convex_qp( dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); dense::QP<T> qp(dim, n_eq, n_in); // create the QP object qp.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u); // initialize the model qp.solve(); // solve the problem // a new qp problem dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); // re update the model // solve it qp.solve(); // print an optimal solution x,y and z std::cout << "optimal x: " << qp.results.x << std::endl; std::cout << "optimal y: " << qp.results.y << std::endl; std::cout << "optimal z: " << qp.results.z << std::endl; // if you have boxes (dense backend only) you proceed the same way dense::Vec<T> u_box(dim); dense::Vec<T> l_box(dim); u_box.setZero(); l_box.setZero(); u_box.array() += 1.E10; l_box.array() -= 1.E10; qp_box.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u, l_box, u_box); qp_box.solve(); u_box.array() += 1.E1; l_box.array() -= 1.E1; qp_box.solve(); } | import proxsuite import numpy as np from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.dense.QP(n, n_eq, n_in) # generate a random QP H, g, A, b, C, u, l = generate_mixed_qp(n) # initialize the model of the problem to solve qp.init(H, g, A, b, C, l, u) # solve without warm start qp.solve() # create a new problem and update qp H_new, g_new, A_new, b_new, C_new, u_new, l_new = generate_mixed_qp(n, seed=2) qp.update(H_new, g_new, A_new, b_new, C_new, l_new, u_new) # solve it qp.solve() # print an optimal solution print("optimal x: {}".format(qp.results.x)) print("optimal y: {}".format(qp.results.y)) print("optimal z: {}".format(qp.results.z)) # if you have boxes (dense backend only) you proceed the same way qp2 = proxsuite.proxqp.dense.QP(n, n_eq, n_in, True) l_box = -np.ones(n) * 1.0e10 u_box = np.ones(n) * 1.0e10 qp2.init(H, g, A, b, C, l, u, l_box, u_box) qp2.solve() l_box += 1.0e1 u_box -= 1.0e1 qp2.update(H_new, g_new, A_new, b_new, C_new, l_new, u_new, l_box, u_box) qp2.solve() |
Contrary to the init method, the compute_preconditioner boolean parameter becomes update_preconditioner, which enables you to keep the previous preconditioner (if set to false) to equilibrate the new updated problem, or to re-compute the preconditioner with the new values of the problem (if set to true). By default, the update_preconditioner parameter is set to false.
The major difference between the dense and sparse API is that in the sparse case only if you change the matrices of the model, the update will take effect only if the matrices have the same sparsity structure (i.e., the non-zero values are located at the same place). Hence, if the matrices have a different sparsity structure, you must create a new Qp object to solve the new problem. We provide an example below.
| examples/cpp/update_sparse_qp.cpp | examples/python/update_sparse_qp.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/sparse/sparse.hpp> // get the sparse API of ProxQP #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite; using namespace proxsuite::proxqp; using T = double; int main() { isize n = 10; isize n_eq(n / 4); isize n_in(n / 4); T p = 0.15; // level of sparsity T conditioning = 10.0; // conditioning level for H n, conditioning, p); auto g = ::proxsuite::proxqp::utils::rand::vector_rand<T>(n); auto A = ::proxsuite::proxqp::utils::rand::sparse_matrix_rand<T>(n_eq, n, p); auto C = ::proxsuite::proxqp::utils::rand::sparse_matrix_rand<T>(n_in, n, p); auto x_sol = ::proxsuite::proxqp::utils::rand::vector_rand<T>(n); auto b = A * x_sol; auto l = C * x_sol; auto u = (l.array() + 10).matrix().eval(); // design a qp2 object using sparsity masks of H, A and C H.cast<bool>(), A.cast<bool>(), C.cast<bool>()); qp.init(H, g, A, b, C, l, u); qp.solve(); // update H auto H_new = 2 * H; // keep the same sparsity structure qp.update(H_new, nullopt); // update H with H_new, it will work qp.solve(); // generate H2 with another sparsity structure n, conditioning, p); qp.update(H2, nullopt); // nothing will happen // if only a vector changes, then the update takes effect auto g_new = ::proxsuite::proxqp::utils::rand::vector_rand<T>(n); qp.solve(); // it solves the problem with another vector // to solve the problem with H2 matrix create a new qp object H2.cast<bool>(), A.cast<bool>(), C.cast<bool>()); qp2.init(H2, g_new, A, b, C, l, u); qp2.solve(); // it will solve the new problem // print an optimal solution x,y and z std::cout << "optimal x: " << qp2.results.x << std::endl; std::cout << "optimal y: " << qp2.results.y << std::endl; std::cout << "optimal z: " << qp2.results.z << std::endl; } Definition common.hpp:14 | import proxsuite from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.sparse.QP(n, n_eq, n_in) # generate a random QP H, g, A, b, C, u, l = generate_mixed_qp(n, True) # initialize the model of the problem to solve qp.init(H, g, A, b, C, l, u) qp.solve() H_new = 2 * H # keep the same sparsity structure qp.update(H_new) # update H with H_new, it will work qp.solve() # generate a QP with another sparsity structure # create a new problem and update qp H2, g_new, A_new, b_new, C_new, u_new, l_new = generate_mixed_qp(n, True) qp.update(H=H2) # nothing will happen qp.update(g=g_new) # if only a vector changes, then the update takes effect qp.solve() # it solves the problem with the QP H,g_new,A,b,C,u,l # to solve the problem with H2 matrix create a new qp object in the sparse case qp2 = proxsuite.proxqp.sparse.QP(n, n_eq, n_in) qp2.init(H2, g_new, A, b, C, l, u) qp2.solve() # it will solve the new problem # print an optimal solution print("optimal x: {}".format(qp.results.x)) print("optimal y: {}".format(qp.results.y)) print("optimal z: {}".format(qp.results.z)) |
Finally, if you want to change your initial guess option when updating the problem, you must change it in the setting before the update takes effect for the next solve (otherwise, it will keep the previous one set). It is important, especially for the WARM_START_WITH_PREVIOUS_RESULT initial guess option (set by default in the solver). Indeed, in this case, if no matrix is updated, the workspace keeps the previous factorization in the update method, which adds considerable speed up for the next solving. We provide below an example in the dense case.
| examples/cpp/update_dense_qp_ws_previous_result.cpp | examples/python/update_dense_qp_ws_previous_result.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/dense/dense.hpp> // load the dense solver backend #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite; using namespace proxsuite::proxqp; using T = double; int main() { dense::isize dim = 10; dense::isize n_eq(dim / 4); dense::isize n_in(dim / 4); // generate a random qp T sparsity_factor(0.15); T strong_convexity_factor(1.e-2); dense::Model<T> qp_random = utils::dense_strongly_convex_qp( dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); dense::QP<T> qp(dim, n_eq, n_in); // create the QP object qp.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u); // initialize the model qp.solve(); // solve the problem // re update the linear cost taking previous result qp.settings.initial_guess = // it takes effect at the update because it is set before // (the workspace is not erased at the update method, hence // the previous factorization is kept) // a new linear cost slightly modified auto g = qp_random.g * 0.95; qp.solve(); // print an optimal solution x,y and z std::cout << "optimal x: " << qp.results.x << std::endl; std::cout << "optimal y: " << qp.results.y << std::endl; std::cout << "optimal z: " << qp.results.z << std::endl; } @ WARM_START_WITH_PREVIOUS_RESULT Definition status.hpp:32 | import proxsuite from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.dense.QP(n, n_eq, n_in) # generate a random QP H, g, A, b, C, u, l = generate_mixed_qp(n) # initialize the model of the problem to solve qp.init(H, g, A, b, C, l, u) # solve without warm start qp.solve() # create a new problem and update qp g_new = 0.95 * g # slightly different g_new qp.settings.initial_guess = ( proxsuite.proxqp.InitialGuess.WARM_START_WITH_PREVIOUS_RESULT ) qp.update(g=g_new) # solve it qp.solve() # print an optimal solution print("optimal x: {}".format(qp.results.x)) print("optimal y: {}".format(qp.results.y)) print("optimal z: {}".format(qp.results.z)) |
Note that one subsection is dedicated to the different initial guess options below.
In this section, you will find first the solver's settings and then a subsection detailing the different initial guess options.
In this table, you have the three columns from left to right: the name of the setting, its default value, and then a short description of it.
| Setting | Default value | Description |
|---|---|---|
| eps_abs | 1.E-5 | Asbolute stopping criterion of the solver. |
| eps_rel | 0 | Relative stopping criterion of the solver. |
| check_duality_gap | False | If set to true, include the duality gap in absolute and relative stopping criteria. |
| eps_duality_gap_abs | 1.E-4 | Asbolute duality-gap stopping criterion of the solver. |
| eps_duality_gap_rel | 0 | Relative duality-gap stopping criterion of the solver. |
| VERBOSE | False | If set to true, the solver prints information at each loop. |
| default_rho | 1.E-6 | Default rho parameter of result class (i.e., for each initial guess, except WARM_START_WITH_PREVIOUS_RESULT, after a new solve or update, the solver initializes rho to this value). |
| default_mu_eq | 1.E-3 | Default mu_eq parameter of result class (i.e., for each initial guess, except WARM_START_WITH_PREVIOUS_RESULT, after a new solve or update, the solver initializes mu_eq to this value). |
| default_mu_in | 1.E-1 | Default mu_in parameter of result class (i.e., for each initial guess, except WARM_START_WITH_PREVIOUS_RESULT, after a new solve or update, the solver initializes mu_in to this value). |
| compute_timings | False | If set to true, timings in microseconds will be computed by the solver (setup time, solving time, and run time = setup time + solving time). |
| max_iter | 10.000 | Maximal number of authorized outer iterations. |
| max_iter_in | 1500 | Maximal number of authorized inner iterations. |
| initial_guess | EQUALITY_CONSTRAINED_INITIAL_GUESS | Sets the initial guess option for initilizing x, y and z. |
| mu_min_eq | 1.E-9 | Minimal authorized value for mu_eq. |
| mu_min_in | 1.E-8 | Minimal authorized value for mu_in. |
| mu_update_factor | 0.1 | Factor used for updating mu_eq and mu_in. |
| eps_primal_inf | 1.E-4 | Threshold under which primal infeasibility is detected. |
| eps_dual_inf | 1.E-4 | Threshold under which dual infeasibility is detected. |
| update_preconditioner | False | If set to true, the preconditioner will be re-derived with the update method, otherwise it uses previous one. |
| compute_preconditioner | True | If set to true, the preconditioner will be derived with the init method. |
| alpha_bcl | 0.1 | alpha parameter of the BCL algorithm. |
| beta_bcl | 0.9 | beta parameter of the BCL algorithm. |
| refactor_dual_feasibility_threshold | 1.E-2 | Threshold above which refactorization is performed to change rho parameter. |
| refactor_rho_threshold | 1.E-7 | New rho parameter used if the refactor_dual_feasibility_threshold condition has been satisfied. |
| cold_reset_mu_eq | 1./1.1 | Value used for cold restarting mu_eq. |
| cold_reset_mu_in | 1./1.1 | Value used for cold restarting mu_in. |
| nb_iterative_refinement | 10 | Maximal number of iterative refinements. |
| eps_refact | 1.E-6 | Threshold value below which the Cholesky factorization is refactorized factorization in the iterative refinement loop. |
| safe_guard | 1.E4 | Safeguard parameter ensuring global convergence of the scheme. More precisely, if the total number of iterations is superior to safe_guard, the BCL scheme accepts always the multipliers (hence the scheme is a pure proximal point algorithm). |
| preconditioner_max_iter | 10 | Maximal number of authorized iterations for the preconditioner. |
| preconditioner_accuracy | 1.E-3 | Accuracy level of the preconditioner. |
| HessianType | Dense | Defines the type of problem solved (Dense, Zero, or Diagonal). In case the Zero or Diagonal option is used, the solver exploits the Hessian structure to evaluate the Cholesky factorization efficiently. |
| primal_infeasibility_solving | False | If set to true, it solves the closest primal feasible problem if primal infeasibility is detected. |
| nb_power_iteration | 1000 | Number of power iteration iteration used by default for estimating H lowest eigenvalue. |
| power_iteration_accuracy | 1.E-6 | If set to true, it solves the closest primal feasible problem if primal infeasibility is detected. |
| primal_infeasibility_solving | False | Accuracy target of the power iteration algorithm for estimating the lowest eigenvalue of H. |
| estimate_method_option | NoRegularization | Option for estimating the minimal eigen value of H and regularizing default_rho default_rho=rho_regularization_scaling*abs(default_H_eigenvalue_estimate). This option can be used for solving non convex QPs. |
| default_H_eigenvalue_estimate | 0. | Default estimate of the minimal eigen value of H. |
| rho_regularization_scaling | 1.5 | Scaling for regularizing default_rho according to the minimal eigen value of H. |
The solver has five different possible initial guesses for warm starting or not the initial iterate values:
The different options will be commented below in the introduced order above.
If set to this option, the solver will start with no initial guess, which means that the initial values of x, y, and z are the 0 vector.
The solver environment provides an independent function for estimating the minimal eigenvalue of a dense or sparse symmetric matrix. It is named "estimate_minimal_eigen_value_of_symmetric_matrix". In the sparse case, it uses a power iteration algorithm (with two options: the maximal number of iterations and the accuracy target for the estimate). In the dense case, we provide two options within the struct EigenValueEstimateMethodOption:
Estimating minimal eigenvalue is particularly usefull for solving QP with non convex quadratics. Indeed, if default_rho is set to a value strictly higher than the minimal eigenvalue of H, then ProxQP is guaranteed for find a local minimum to the problem since it relies on a Proximal Method of Multipliers (for more detail for example this work providing convergence proof of this property).
More precisely, ProxQP API enables the user to provide for the init or update methods estimate of the minimal eigenvalue of H (i.e., manual_minimal_H_eigenvalue). It the values are not empty, then the values of primal proximal step size rho will be updated according to: rho = rho + abs(manual_minimal_H_eigenvalue). It guarantees that the proximal step-size is larger than the minimal eigenvalue of H and hence to converging towards a local minimum of the QP. We provide below examples in C++ and python for using this feature appropriately with the dense backend (it is similar with the sparse one)
| examples/cpp/estimate_nonconvex_eigenvalue.cpp | examples/python/estimate_nonconvex_eigenvalue.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/dense/dense.hpp> // load the dense solver backend #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite; using namespace proxsuite::proxqp; using T = double; int main() { dense::isize dim = 10; dense::isize n_eq(dim / 4); dense::isize n_in(dim / 4); // generate a random qp T sparsity_factor(0.15); T strong_convexity_factor(1.e-2); dense::Model<T> qp_random = utils::dense_strongly_convex_qp( dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); // make the QP nonconvex dense::Vec<T> diag(dim); diag.setOnes(); qp_random.H.diagonal().array() -= 2. * diag.array(); // add some nonpositive values dense matrix Eigen::SelfAdjointEigenSolver<dense::Mat<T>> es(qp_random.H, Eigen::EigenvaluesOnly); T minimal_eigenvalue = T(es.eigenvalues().minCoeff()); // choose scaling for regularizing default_rho accordingly dense::QP<T> qp(dim, n_eq, n_in); // create the QP object // choose the option for estimating this eigenvalue T estimate_minimal_eigen_value = qp_random.H, EigenValueEstimateMethodOption::ExactMethod, 1.E-6, 10000); bool compute_preconditioner = false; // input the estimate for making rho appropriate qp.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u, compute_preconditioner, estimate_minimal_eigen_value); // print the estimates std::cout << "ProxQP estimate " << qp.results.info.minimal_H_eigenvalue_estimate << std::endl; std::cout << "minimal_eigenvalue " << minimal_eigenvalue << std::endl; std::cout << "default_rho " << qp.settings.default_rho << std::endl; } T estimate_minimal_eigen_value_of_symmetric_matrix(const Eigen::MatrixBase< MatIn > &H, EigenValueEstimateMethodOption estimate_method_option, T power_iteration_accuracy, isize nb_power_iteration) Definition helpers.hpp:125 @ ExactMethod Definition settings.hpp:50 | import proxsuite import numpy as np import scipy.sparse as spa from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.dense.QP(n, n_eq, n_in) # generate a random non convex QP H, g, A, b, C, u, l = generate_mixed_qp(n) # initialize the model of the problem to solve estimate_minimal_eigen_value = ( H, proxsuite.proxqp.EigenValueEstimateMethodOption.ExactMethod ) ) qp.init(H, g, A, b, C, l, u, manual_minimal_H_eigenvalue=estimate_minimal_eigen_value) vals, _ = spa.linalg.eigs(H, which="SR") min_eigenvalue = float(np.min(vals)) # print the estimates print(f"{min_eigenvalue=}") print(f"{estimate_minimal_eigen_value=}") print(f"{qp.results.info.minimal_H_eigenvalue_estimate=}") |
If set to this option, the solver will start with no initial guess, which means that the initial values of x, y, and z are the 0 vector.
If set to this option, the solver will solve at the beginning the following system for warm starting x and y.
z stays to 0. In general, this option warm starts well equality constrained QP.
If set to this option, the solver will warm start x, y, and z with the values of the previous problem solved and it will keep all the last parameters of the solver (i.e., proximal step sizes, for example, and the full workspace with the Cholesky factorization etc.). Hence, if the new problem to solve is the same as the previous one, the problem is warm-started at the solution (and zero iteration will be executed).
This option was initially thought to be used in optimal control-like problems when the next problem to be solved is close to the previous one. Indeed, if the problem changes only slightly, it is reasonable to warm start the new problem with the value of the previous one for speeding the whole runtime.
Note, however, that if your update involves new matrices or you decide to change parameters involved in the Cholesky factorization (i.e., the proximal step sizes), then for consistency, the solver will automatically refactorize the Cholesky with these updates (and taking into account the last values of x, y, and z for the active set).
Finally, note that this option is set by default in the solver. At the first solve, as there is no previous results, x, y and z are warm started with the 0 vector value.
If set to this option, the solver expects then a warm start at the solve method.
Note, that it is unnecessary to set this option through the command below (for example, in C++) before the update or solve method call.
It is sufficient to just add the warm start in the solve method, and the solver will automatically make the setting change internally.
If set to this option, the solver will warm start x, y and z with the values of the previous problem solved. Contrary to the WARM_START_WITH_PREVIOUS_RESULT option, all other parameters of the solver (i.e., proximal step sizes for example, and the full workspace with the ldlt factorization etc.) are re-set to their default values (hence a factorization is reperformed taking into account of z warm start for the active set, but with default values of proximal step sizes).
This option has also been thought initially for being used in optimal control-like problems when the next problem to be solved is close to the previous one. Indeed, if the problem changes only slightly, it is reasonable to warm start the new problem with the value of the previous one for speeding the whole runtime.
Note finally that at the first solve, as there are no previous results, x, y, and z are warm started with the 0 vector value.
The result subclass is composed of the following:
If the solver has solved the problem, the triplet (x,y,z) satisfies:
accordingly with the parameters eps_abs and eps_rel chosen by the user.
If the problem is primal infeasible and you have enabled the solver to solve the closest feasible problem, then (x,y,z) will satisfy.
(se, si) stands in this context for the optimal shifts in
defines a primal feasible problem.
Note that if you use the dense backend and its specific feature for handling box inequality constraints, then the first
In this table you have on the three columns from left to right: the name of the info subclass item, its default value, and then a short description of it.
| Info item | Default value | Description |
|---|---|---|
| mu_eq | 1.E-3 | Proximal step size wrt equality constraints multiplier. |
| mu_in | 1.E-1 | Proximal step size wrt inequality constraints multiplier. |
| rho | 1.E-6 | Proximal step size wrt primal variable. |
| iter | 0 | Total number of iterations. |
| iter_ext | 0 | Total number of outer iterations. |
| mu_updates | 0 | Total number of mu updates. |
| rho_updates | 0 | Total number of rho updates. |
| status | PROXQP_NOT_RUN | Status of the solver. |
| setup_time | 0 | Setup time (takes into account the equilibration procedure). |
| solve_time | 0 | Solve time (takes into account the first factorization). |
| run_time | 0 | the sum of the setup time and the solve time. |
| objValue | 0 | The objective value to minimize. |
| pri_res | 0 | The primal residual. |
| dua_res | 0 | The dual residual. |
Note finally that when initializing a QP object, by default, the proximal step sizes (i.e., rho, mu_eq, and mu_in) are set up by the default values defined in the Setting class. Hence, when doing multiple solves, if not specified, their values are re-set respectively to default_rho, default_mu_eq, and default_mu_in. A small example is given below in C++ and Python.
| examples/cpp/init_with_default_options.cpp | examples/python/init_with_default_options.py |
|---|---|
#include <iostream> #include <proxsuite/proxqp/dense/dense.hpp> // load the dense solver backend #include <proxsuite/proxqp/utils/random_qp_problems.hpp> // used for generating a random convex qp using namespace proxsuite::proxqp; using T = double; int main() { isize dim = 10; isize n_eq(dim / 4); isize n_in(dim / 4); // generate a random qp T sparsity_factor(0.15); T strong_convexity_factor(1.e-2); dense::Model<T> qp_random = utils::dense_strongly_convex_qp( dim, n_eq, n_in, sparsity_factor, strong_convexity_factor); dense::QP<T> qp( dim, n_eq, n_in); // create the QP // initialize the model, along with another rho parameter qp.settings.initial_guess = InitialGuessStatus::NO_INITIAL_GUESS; qp.init(qp_random.H, qp_random.g, qp_random.A, qp_random.b, qp_random.C, qp_random.l, qp_random.u, true, /*rho*/ 1.e-7, /*mu_eq*/ 1.e-4); // Initializing rho sets in practive qp.settings.default_rho value, // hence, after each solve or update method, the qp.results.info.rho value // will be reset to qp.settings.default_rho value. qp.solve(); // So if we redo a solve, qp.settings.default_rho value = 1.e-7, hence // qp.results.info.rho restarts at 1.e-7 The same occurs for mu_eq. qp.solve(); // There might be a different result with WARM_START_WITH_PREVIOUS_RESULT // initial guess option, as by construction, it reuses the last proximal step // sizes of the last solving method. } | import proxsuite from util import generate_mixed_qp # load a qp object using qp problem dimensions n = 10 n_eq = 2 n_in = 2 qp = proxsuite.proxqp.dense.QP(n, n_eq, n_in) # generate a random QP H, g, A, b, C, u, l = generate_mixed_qp(n) # initialize the model of the problem to solve qp.init(H, g, A, b, C, l, u, rho=1.0e-7, mu_eq=1.0e-4) qp.solve() # If we redo a solve, qp.settings.default_rho value = 1.e-7, hence qp.results.info.rho restarts at 1.e-7 # The same occurs for mu_eq. qp.solve() # There might be a different result with WARM_START_WITH_PREVIOUS_RESULT initial guess option, as # by construction, it reuses the last proximal step sizes of the last solving method. |
The solver has five status:
Infeasibility is detected using the necessary conditions exposed in section 3.4. More precisely, primal infeasibility is assumed if the following conditions are matched for some non zeros dy and dz (according to the eps_prim_inf variable set by the user):
Dual infeasibility is assumed if the following two conditions are matched for some non-zero dx (according to the eps_dual_inf variable set by the user):
If the problem turns out to be primal or dual infeasible, then x, y, and z stored in the results class will be the certificate of primal or dual infeasibility. More precisely:
We have the following generic advice for choosing between the sparse and dense backend. If your problem is not:
then we recommend using the solver with the dense backend.
The sparsity ratio of matrix A is defined as:
which accounts for the percentage of non-zero elements in matrix A.
We first provide some details about what is measured in the setup and solve time of ProxQP, which is of some importance when doing benchmarks with other solvers, as they can measure different things in a feature with a similar name.
Then we conclude this documentation section with some compilation options for ProxQP which can considerably speed up the solver, considering your OS architecture.
An important remark about quadratic programming solver is that they all rely at some point on a factorization matrix algorithm, which constitutes the time bottleneck of the solver (as the factorization has a cubic order of complexity wrt dimension of the matrix to factorize).
Available solvers often have a similar API as the one we propose, with first an "init" method for initializing the model, and then a "solve" method for solving the QP problem. For not biasing the benchmarks, it is important to know where is done the first matrix factorization, as it constitutes a considerable cost. In our API, we have decided to make the first factorization in the solve method, as we consider that factorizing the problem is part of the solving part. Hence in terms of timing:
It is important to notice that some other solvers API have made different choices. For example, OSQP measures in the setup time the first factorization of the system (at the time ProxQP algorithm was published). Hence we recommend that for benchmarking ProxQP against other solvers, you should compare ProxQP runtime against the other solvers' runtime (i.e., everything from what constitutes their setup to their solve method). Otherwise, the benchmarks won't take into comparable account timings.
We highly encourage you to enable the vectorization of the underlying linear algebra for the best performance. You just need to activate the cmake option BUILD_WITH_SIMD_SUPPORT=ON, like:
ProxQP can be compiled more precisely with two SIMD instructions options for x86 instruction set architectures: AVX-2 and AVX-512. They can considerably enhance the speed of ProxQP, and we encourage you to use them if your OS is compatible with them. For your information, our benchmarks for ProxQP algorithm have been realised with AVX-2 compilation option.