Introduction
This guide provides an overview of the PIPS-IPM++ C API for solving large-scale optimization problems with a bordered block-diagonal structure. The API wraps main functions of the C++ API and allows providing the problem data using a flexible callback mechanism.
All available functions and types are defined in PIPSIPMppInterface.h and TerminationStatus.h.
Instead of building the entire problem matrix in memory, PIPS-IPM++ uses a "callback" mechanism. The solver executes user-provided "callbacks" to request the data it needs at a given time. See The Callback Mechanism regarding the list of callbacks.
Step-by-Step Guide
The file Drivers/CallbackExample/callbackExample.c provides a minimal, self-contained example demonstrating the C-style callback API for a simple two-stage problem. This is the best place to start to understand the basic mechanics. The problem data is hard-coded directly inside the callback functions.
Here is a summary of the steps taken in callbackExample.c.
Step 1: Define the Callback Functions
First, one needs to write the callbacks that provide the problem data.
Let's look at a few key callbacks from callbackExample.c.
- cb_n: Provides the number of variables for a given node.
int cb_n(void* user_data, int id, int* nnz) {
*nnz = (id == 2) ? 4 : 2;
return 0;
}
- cb_nnzA: Provides the number of non-zeros in the A matrix (first-stage equality constraints).
int cb_nnzA(void* user_data, int id, int* nnz) {
*nnz = 2;
return 0;
}
- cb_b: Fills a vector with the right-hand-side values for the equality constraints.
int cb_b(void* user_data, int id, double* vec, int len) {
if (id == 0) {
vec[0] = 2.0;
vec[1] = 7.0;
}
else if (id == 1) {
vec[0] = 3.0;
vec[1] = 7.0;
}
else if (id == 2) {
vec[0] = 2.0;
vec[1] = 7.0;
}
return 0;
}
- cb_A: Provides the A matrix data in compressed row storage (CRS) format.
int cb_A(void* user_data, int id, int* krow, int* jcol, double* M) {
if (id == 0) {
M[0] = 2.0;
M[1] = 7.0;
}
else if (id == 1) {
M[0] = 2.0;
M[1] = 5.0;
}
else {
M[0] = 2.0;
M[1] = 4.0;
}
krow[0] = 0;
krow[1] = 1;
krow[2] = 2;
jcol[0] = 0;
jcol[1] = 1;
return 0;
}
Step 2: Build the Problem Tree
Once the callbacks have been defined, a tree that represents the problem structure needs to be constructed. For the C interface, this has been simplified to specify the number of blocks, a set of callbacks for the block 0 (root node), and a set of callbacks for any other block (leaf nodes).
Here's how it's done in callbackExample.c:
int nBlocks = 2;
ProbData prob_data = {nBlocks};
.n = cb_n,
.my = cb_my,
.myl = cb_myl,
.mz = cb_mz,
...
.ixlow = cb_ixlow,
.ixupp = cb_vec_zero,
.ixtyp = NULL,
.objconst = NULL,
.user_data = &prob_data};
}
void * pips_tree_create(int num_blocks, const PipsNodeCallbacks *root_cb, const PipsNodeCallbacks *block_cb)
Definition PIPSIPMppInterface.h:53
The user_data in PipsNodeCallbacks is a void* for user data, which is then passed to every callback. This is useful for passing problem parameters or other data.
Step 3: Create and Run the Solver
After building the tree, an instance of the solver is created, passing it the tree. In addition, parameters can be set. Then, the pips_solve function is called.
int rank;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
if (rank == 0)
printf("solving...\n");
void * pips_solver_create(void *tree, const char *options_file)
int pips_set_bool_option(const char *name, bool value)
PipsTerminationStatus pips_solve(void *solver)
int pips_set_string_option(const char *name, const char *value)
PipsTerminationStatus
the termination status of a PIPS-IPM++ solve
Definition TerminationStatus.h:9
Since the same code runs in every MPI process, a check has been added here to make sure that solving... is printed only in the first process.
The PipsTerminationStatus indicates whether the problem could be solved to optimality, unboundedness or infeasibility was detected, or the solve stopped due to a working limit or numerical problems.
Step 4: Retrieve Results
After pips_solve completes, one can query the solver for the results.
if (rank == 0)
printf("solving finished ... objective value: %g\n", objective);
double* primal = NULL;
int primal_size = 0;
double* dual_vb = NULL;
int dual_vb_size = 0;
double* eq_values = NULL;
int eq_values_size = 0;
double* ineq_values = NULL;
int ineq_values_size = 0;
double* dual_eq = NULL;
int dual_eq_size = 0;
double* dual_ineq = NULL;
int dual_ineq_size = 0;
int pips_get_dual_eq(void *solver, double **out, int *size)
double pips_get_objective(void *solver)
int pips_get_inequality_values(void *solver, double **out, int *size)
int pips_get_dual_ineq(void *solver, double **out, int *size)
int pips_get_equality_values(void *solver, double **out, int *size)
int pips_get_primal(void *solver, double **out, int *size)
int pips_get_dual_var_bounds(void *solver, double **out, int *size)
Step 5: Cleanup
Finally, the memory of the solver and the tree should be freed:
void pips_solver_destroy(void *solver)
void pips_tree_destroy(void *tree)