Problem Formulation
PIQP expects QP of the form
\[\begin{aligned} \min_{x} \quad & \frac{1}{2} x^\top P x + c^\top x \\ \text {s.t.}\quad & Ax=b, \\ & Gx \leq h, \\ & x_{lb} \leq x \leq x_{ub} \end{aligned}\]with primal decision variables \(x \in \mathbb{R}^n\), matrices \(P\in \mathbb{S}_+^n\), \(A \in \mathbb{R}^{p \times n}\), \(G \in \mathbb{R}^{m \times n}\), and vectors \(c \in \mathbb{R}^n\), \(b \in \mathbb{R}^p\), \(h \in \mathbb{R}^m\), \(x_{lb} \in \mathbb{R}^n\), and \(x_{ub} \in \mathbb{R}^n\).
PIQP can handle infinite box constraints well, i.e. when elements of \(x_{lb}\) or \(x_{ub}\) are \(-\infty\) or \(\infty\), respectively. On the contrary, infinite values in the general inequalities \(Gx \leq h = \pm \infty\) can cause problems. PIQP internally disables them by setting the corresponding rows in \(G\) to zero (sparsity structure is preserved). For best performance, consider removing the corresponding constraints from the problem formulation directly.
Example QP
In the following the C++ interface of PIQP will be introduced using the following example QP problem:
\[\begin{aligned} \min_{x} \quad & \frac{1}{2} x^\top \begin{bmatrix} 6 & 0 \\ 0 & 4 \end{bmatrix} x + \begin{bmatrix} -1 \\ -4 \end{bmatrix}^\top x \\ \text {s.t.}\quad & \begin{bmatrix} 1 & -2 \end{bmatrix} x = 1, \\ & \begin{bmatrix} 1 & -1 \\ 2 & 0 \end{bmatrix} x \leq \begin{bmatrix} 0.2 \\ -1 \end{bmatrix}, \\ & -1 \leq x_1 \leq 1. \end{aligned}\]Problem Data
PIQP supports dense and sparse problem formulations. For small and dense problems the dense interface is preferred since vectorized instructions and cache locality can be exploited more efficiently, but for sparse problems the sparse interface and result in significant speedups.
To include the C interface of PIQP include the following headers
#include "stdlib.h"
#include "piqp.h"
stdlib.h
is later needed to allocate necessary structs.
We can then define the problem data as
piqp_int n = 2;
piqp_int p = 1;
piqp_int m = 2;
piqp_float P[4] = {6, 0, 0, 4};
piqp_float c[2] = {-1, -4};
piqp_float A[2] = {1, -2};
piqp_float b[1] = {1};
piqp_float G[4] = {1, -1, 2, 0};
piqp_float h[2] = {0.2, -1};
piqp_float x_lb[2] = {-1, -PIQP_INF};
piqp_float x_ub[2] = {1, PIQP_INF};
piqp_data_sparse* data = (piqp_data_sparse*) malloc(sizeof(piqp_data_sparse));
data->n = n;
data->p = p;
data->m = m;
data->P = P;
data->c = c;
data->A = A;
data->b = b;
data->G = G;
data->h = h;
data->x_lb = x_lb;
data->x_ub = x_ub;
Here PIQP_INF
represents \(\infty\), and we store the whole problem in the data
struct.
For the sparse interface \(P\), \(A\), and \(G\) have to be in compressed sparse column (CSC) format.
piqp_float P_x[2] = {6, 4};
piqp_int P_nnz = 2;
piqp_int P_p[3] = {0, 1, 2};
piqp_int P_i[2] = {0, 1};
piqp_float A_x[2] = {1, -2};
piqp_int A_nnz = 2;
piqp_int A_p[3] = {0, 1, 2};
piqp_int A_i[2] = {0, 0};
piqp_float G_x[3] = {1, 2, -1};
piqp_int G_nnz = 3;
piqp_int G_p[3] = {0, 2, 3};
piqp_int G_i[4] = {0, 1, 0};
data->P = piqp_csc_matrix(data->n, data->n, P_nnz, P_p, P_i, P_x);
data->A = piqp_csc_matrix(data->p, data->n, A_nnz, A_p, A_i, A_x);
data->G = piqp_csc_matrix(data->m, data->n, G_nnz, G_p, G_i, G_x);
piqp_csc_matrix(...)
is a helper function allocating a piqp_csc
struct and filling its fields accordingly.
Every member in data
, except P
and c
, is optional and may be NULL
.
Settings
To set custom settings, a piqp_settings
struct has to be instantiated and the default settings have to be set:
piqp_settings* settings = (piqp_settings*) malloc(sizeof(piqp_settings));
piqp_set_default_settings(settings);
settings->verbose = 1;
settings->compute_timings = 1;
In this example we enable the verbose output and internal timings. The full set of configuration options can be found here.
Solving the Problem
We can now set up the problem using
// workspace
piqp_workspace* work;
// dense interface
piqp_setup_dense(&work, data, settings);
// or sparse interface
piqp_setup_sparse(&work, data, settings);
The data is internally copied, and the solver initializes all internal data structures. Note that the settings field is optional and NULL
can be passed.
Now, the problem can be solver using
piqp_status status = piqp_solve(work);
Status code
Status Code | Value | Description |
---|---|---|
PIQP_SOLVED | 1 | Solver solved problem up to given tolerance. |
PIQP_MAX_ITER_REACHED | -1 | Iteration limit was reached. |
PIQP_PRIMAL_INFEASIBLE | -2 | The problem is primal infeasible. |
PIQP_DUAL_INFEASIBLE | -3 | The problem is dual infeasible. |
PIQP_NUMERICS | -8 | Numerical error occurred during solving. |
PIQP_UNSOLVED | -9 | The problem is unsolved, i.e., solve was never called. |
PIQP_INVALID_SETTINGS | -10 | Invalid settings were provided to the solver. |
Extracting the Result
The result of the optimization can be obtained from the work->result
struct. More specifically, the most important information includes
work->result->x
: primal solutionwork->result->y
: dual solution of equality constraintswork->result->z
: dual solution of inequality constraintswork->result->z_lb
: dual solution of lower bound box constraintswork->result->z_ub
: dual solution of upper bound box constraintswork->result->info.primal_obj
: primal objective valuework->result->info.run_time
: total runtime
Timing information like work->result->info.run_time
is only measured if settings->compute_timings
is set to 1
.
Efficient Problem Updates
Instead of creating a new solver object everytime it’s possible to update the problem directly using
// dense interface
piqp_update_dense(&work, P, c, A, b, G, h, x_lb, x_ub);
// or sparse interface
piqp_update_sparse(&work, P, c, A, b, G, h, x_lb, x_ub);
with a subsequent call to
piqp_status status = piqp_solve(work);
This allows the solver to internally reuse memory and factorizations speeding up subsequent solves. Similar to the piqp_setup_*
functions, all parameters are optional and NULL
may be passed instead.
Note the dimension and sparsity pattern of the problem are not allowed to change when calling the piqp_update_*
functions.