|
PIPS-IPM++ Solver and Tools
a parallel interior-point method for doubly bordered block diagonal linear programs
|
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 allows users to define a problem structure and provide data using a flexible callback mechanism.
The core of the API revolves around two main components:
This document will walk through the main concepts and provide a step-by-step guide to using the API, with references to concrete examples.
PIPS-IPM++ is designed to solve large-scale linear programs (LPs) where the constraint matrix has a bordered block-diagonal structure (BlockLP).
In difference to (BlockLP), the structure of the problem that PIPS-IPM++ expects distinguishes equality and inequality constraints, allows arbitrary variable bounds and more variable types, and supports a quadratic objective function. It looks like this:
\begin{align*}\text{minimize}\; & C + \sum_{k=0}^N c_k^T x_k + x_k^T Q_k x_k \\ \text{subject to}\; & \begin{pmatrix} A_0 & & & & \\ A_1 & B_1 & & & \\ A_2 & & B_2 & & \\ \vdots & & & \ddots & \\ A_N & & & & B_N \\ Bl_0 & Bl_1 & Bl_2 & \cdots & Bl_N \\ \end{pmatrix} \begin{pmatrix} x_0 \\ x_1 \\ x_2 \\ \vdots \\ x_N \end{pmatrix} = \begin{pmatrix} b_0 \\ b_1 \\ b_2 \\ \vdots \\ b_N \\ bl \end{pmatrix} \\ & \begin{pmatrix} c_0^{\text{low}} \\ c_1^{\text{low}} \\ c_2^{\text{low}} \\ \vdots \\ c_N^{\text{low}} \\ dl^{\text{low}} \end{pmatrix} \leq \begin{pmatrix} C_0 & & & & \\ C_1 & D_1 & & & \\ C_2 & & D_2 & & \\ \vdots & & & \ddots & \\ C_N & & & & D_N \\ Dl_0 & Dl_1 & Dl_2 & \cdots & Dl_N \\ \end{pmatrix} \begin{pmatrix} x_0 \\ x_1 \\ x_2 \\ \vdots \\ x_N \end{pmatrix} \leq \begin{pmatrix} c_0^{\text{upp}} \\ c_1^{\text{upp}} \\ c_2^{\text{upp}} \\ \vdots \\ c_N^{\text{upp}} \\ dl^{\text{upp}} \end{pmatrix} \\ & x_k^{\text{low}} \leq x_k \leq x_k^{\text{upp}} \quad \forall k \\ & (x_k)_i \in \mathbb{R} \;\text{or}\; \mathbb{Z} \;\text{or}\; \{0,1\} \quad \forall i\,\forall k \end{align*}
where \(C\) is a constant, \(c_k\), \(b_k\), \(c_k^{\text{low}}\), \(c_k^{\text{upp}}\), \(x_k^{\text{low}}\), \(x_k^{\text{upp}}\), \(k=1,\ldots,N\), \(bl\), \(dl^{\text{low}}\), and \(dl^{\text{upp}}\) are vectors, and \(Q_k\), \(A_k\), \(B_k\), \(Bl_k\), \(C_k\), \(D_k\), and \(Dl_k\) are matrices.
In the constraints:
The PIPS-IPM++ API represents this structure as a tree. The root of the tree corresponds to the linking variables/constraints, and each child of the root corresponds to one of the \(N\) blocks.
Note: Even though the API supports a quadratic objective function and non-continuous variables, PIPS-IPM++ can currently only solve continuous linear problems. The \(Q_k\) matrices should therefore always set to be empty and all variables should be declared to be continuous.
The abstract class pipsipmpp::DistributedTree is the cornerstone for representing the problem structure. One needs to implement a derived class of DistributedTree that provides the actual problem data. For convenience, PIPS-IPM++ provides pipsipmpp::DistributedInputTree, which takes C-style function pointers as callbacks.
Each node in the tree is an object that can provide information about its part of the problem (local variables, constraints, and connections to its parent and children).
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. This is a memory-efficient way to handle very large-scale problems.
The primary types of callbacks are:
The callbacks are C-style functions that must match the PipsFNNZ, PipsFMAT, PipsFVEC, or PipsFSCALAR typedefs. When PIPS-IPM++ executes these callbacks, it passes on the block number \(k\) for which it requests data.
The callbacks are passed to the pipsipmpp::DistributedInputTree::DistributedInputNode constructor and have the following correspondence to the above problem representation:
The callbacks for dimensions (n, my, mz, myl, mzl) must always be provided. For all other callbacks, it is possible to pass on nullptr, to indicate that the corresponding scalar, vector, or matrix is zero.
Here is a summary of the steps to define and solve a problem using the C++ API.
First, one needs to write the callbacks that provide the problem data.
A good example is Drivers/CallbackExample/callbackExample.cpp. Let's look at a few key callbacks from that file.
Once the callbacks have been defined, a pipsipmpp::DistributedInputTree that represents the problem structure needs to be constructed.
Here's how it's done in Drivers/CallbackExample/callbackExample.cpp:
The first argument to the pipsipmpp::DistributedInputTree::DistributedInputNode constructor is a void* for user data, which is then passed to every callback. This is useful for passing problem parameters or other data.
After building the tree, an instance of pipsipmpp::PIPSIPMppInterface is created, passing it the tree and the MPI communicator. In addition, parameters can be set by using methods of the pipsipmpp::options namespace. Then, the run() method is called.
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 pipsipmpp::TerminationStatus 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.
After run() completes, one can query the solver for the results.
The pipsipmpp::PIPSIPMppInterface class is the primary user-facing class for interacting with the PIPS-IPM++ solver. It encapsulates the entire solution process, including reading the problem structure from the pipsipmpp::DistributedTree, presolving, solving with the core interior-point algorithm, and postsolving to return the solution in terms of the original problem.
Below are some of the most important methods available in pipsipmpp::PIPSIPMppInterface.
PIPSIPMppInterface provides a comprehensive set of methods to gather different parts of the solution from all MPI processes to the root process (rank 0). These methods typically return a std::vector<double>.
There are many more gather... methods to retrieve specific components of the primal and dual solutions, as well as slacks and residuals. Refer to pipsipmpp::PIPSIPMppInterface for a complete list.
The file Drivers/CallbackExample/callbackExample.cpp 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.
The file Drivers/gams/gmspips/gmspips_reader.cpp shows a more complex, real-world example of how to interface an external modeling system, in this case GAMS (the General Algebraic Modeling System), with PIPS-IPM++.
In this setup, the optimization problem is not defined in C++ code but is instead read from GDX (GAMS Data Exchange) files. GDX files are binary files that store GAMS model data (parameters, variables, equations, etc.). This approach allows modelers to use the high-level GAMS language to define their large-scale LPs with bordered block diagonal structure, with PIPS-IPM++ acting as the solver engine. See Running on GDX input for more information on this workflow.
The gmspips_reader serves as a bridge, reading the GDX files and translating the data into a format that PIPS-IPM++ can understand, via the callback interface.
Instead of hard-coding data, the gmspips_reader loads problem data on-demand. Here’s a more detailed look at how it works:
Let's trace how the data for a matrix is provided to the solver:
This example illustrates how the callback API decouples the solver from the data source. The solver does not need to know where the data comes from; it only needs the callbacks to provide it when requested. This makes the API highly flexible for integrating with different modeling languages and file formats.