Multilevel Representations¶
PAO includes several Multilevel Problem Representations (MPRs) that represent multilevel optimization problems with an explicit, compact representation that simplifies the implementation of solvers for bilevel, trilevel and other multilevel optimization problems. These MPRs express objective and constraints using vector and matrix data types. However, they organize these data types to provide a semantically clear organization of multilevel problems. Additionally, the MPRs provide checks to ensure the consistency of the data within and across levels.
The classes LinearMultilevelProblem
and QuadraticMultilevelProblem
respectively
represent linear and quadratic multilevel problems. Although
QuadraticMultilevelProblem
is a generalization of the
representation in LinearMultilevelProblem
, the use of tailored
representations for different classes of problems clarifies the semantic
context when using them. For example, this allows for simple error checking
to confirm that a problem is linear.
Currently, all PAO solvers for MPRs support only linear problems, so the
following sections focus on LinearMultilevelProblem
. However,
we conclude with an example of model transformations that enable the
solution of quadratic problems using QuadraticMultilevelProblem
.
Note
We do not expect many users to directly employ a MPR data representation for their applications. Perhaps this would be desirable if their problem was already represented with matrix and vector data. In general, the algebraic representation supported by Pyomo will be more convenient for large, complex applications.
We expect this representation to be more useful for researchers developing multilevel solvers, since the MPR representations provide structure that simplifies the expression of necessary mathematical operations for these problems.
Linear Bilevel Examples¶
We consider again the bilevel problem PAO1 (1). This problem has linear upper- and lower-level problems with different objectives in each level.
>>> M = pao.mpr.LinearMultilevelProblem()
>>> U = M.add_upper(nxR=2)
>>> L = U.add_lower(nxR=1)
>>> U.x.lower_bounds = [2, np.NINF]
>>> U.x.upper_bounds = [6, np.PINF]
>>> L.x.lower_bounds = [0]
>>> L.x.upper_bounds = [np.PINF]
>>> U.c[U] = [1, 0]
>>> U.c[L] = [3]
>>> L.c[L] = [1]
>>> L.maximize = True
>>> U.equalities = True
>>> U.A[U] = [[1, 1]]
>>> U.b = [10]
>>> L.A[U] = [[ 1, 0],
... [-1, 0],
... [ 1, 0]]
>>> L.A[L] = [[ 1],
... [-4],
... [ 2]]
>>> L.b = [8, -8, 13]
>>> M.check()
>>> opt = pao.Solver("pao.mpr.FA")
>>> results = opt.solve(M)
>>> print(M.U.x.values)
[6.0, 4.0]
>>> print(M.U.LL.x.values)
[2.0]
The example illustrates both the flexibility of the MPR representions
in PAO but also the structure they enforce on the multilevel problem
representation. The upper-level problem is created by calling the
add_upper()
method, which takes arguments that specify the
variables at that level:
nxR
- The number of real variables (Default: 0)nxZ
- The number of general integer variables (Default: 0)nxB
- The number of binary variables (Default: 0)
In each level, the variables are represented as a vector of values, ordered in this manner.
Similarly, the add_lower()
method is used to generate a lower-level
problem from a given level. Note that this allows for the specification
of arbitrary nesting of levels, since a lower-level can be defined
relative to any other level in the model. Additionally, multiple
lower-levels can be specified for relative to a single level (see below).
The add_upper()
and add_lower()
methods return the
corresponding level object, which is used to specify data in the model
later.
For a given level object, Z
, the data Z.x
contains
information about the decision variables. In particular, the values
Z.x.lower_bounds
and Z.x.upper_bounds
can be set with arrays
of numeric values to specify lower- and upper-bounds on the decision
variables. Note that missing lower- and upper-bounds are specified with
numpy.NINF
and numpy.PINF
respectively.
The Z.c
data specifies coefficients of the objective function for
this level. This data is indexed by a level object B
to indicate
the data associated with the variables in B
. In the example above:
U.c[U]
is the array of coefficients of the upper-level objective for the variables in the upper-level,U.c[L]
is the array of coefficients of the upper-level objective for the variables in the lower-level, andL.c[L]
is the array of coefficients of the lower-level objective for the variables in the lower-level.
Since L.c[U]
is not specified, it has a value None
that
indicates that no upper-level variables have non-zero coefficients in the
lower-level objective. The Z.A
data specifies the matrix coefficients
for the constraints using a similar indexing notation and semantics.
The values Z.minimize
and Z.maximize
can be set to True
to indicate whether the objective in Z
minimizes or maximizes.
(The default is minimize.) Similarly the value Z.inequalities
and Z.equalities
can be set to True
to indicate whether the
constraints in Z
are inequalities or equalities. (The default
is inequalities.) Finally, the value Z.b
defines the array of
constraint right-hand-side values.
The :meth:check
method provides a convenient sanity check that the
data is defined consistently within each level and between levels.
Note that PAO supports a consistent interface for creating a solver
interface and for applying solvers. In fact, the user should be
aware that Pyomo and MPR solvers are named in a consistent fashion.
For example, the Pyomo solver pao.pyomo.FA calls the MPR solver
pao.mpr.FA after automatically converting the Pyomo representation
to a :class:LinearMultilevelProblem
representation. This example
illustrates that values Z.x.values
contains the values of each level
Z
after optimization.
Multilevel Examples¶
Multilevel problems can be easily expressed using the same MPR data representation.
Multiple Lower Levels¶
We consider again the bilevel problem PAO2 (2), which has multiple lower-level problems. The PAO2 model can be expressed as a linear multilevel problem as follows:
>>> M = pao.mpr.LinearMultilevelProblem()
>>> U = M.add_upper(nxR=2)
>>> L1 = U.add_lower(nxR=1)
>>> L2 = U.add_lower(nxR=1)
>>> U.x.lower_bounds = [2, np.NINF]
>>> U.x.upper_bounds = [6, np.PINF]
>>> U.c[U] = [1, 0]
>>> U.c[L1] = [3]
>>> U.c[L2] = [3]
>>> U.equalities = True
>>> U.A[U] = [[1, 1]]
>>> U.b = [10]
>>> L1.x.lower_bounds = [0]
>>> L1.x.upper_bounds = [np.PINF]
>>> L1.c[L1] = [1]
>>> L1.maximize = True
>>> L1.A[U] = [[ 1, 0],
... [-1, 0],
... [ 1, 0]]
>>> L1.A[L1] = [[ 1],
... [-4],
... [ 2]]
>>> L1.b = [8, -8, 13]
>>> L2.x.lower_bounds = [0]
>>> L2.x.upper_bounds = [np.PINF]
>>> L2.c[L2] = [1]
>>> L2.maximize = True
>>> L2.A[U] = [[0, 1],
... [0, -1],
... [0, 1]]
>>> L2.A[L2] = [[ 1],
... [-4],
... [ 2]]
>>> L2.b = [8, -8, 13]
>>> opt = pao.Solver("pao.mpr.FA")
>>> results = opt.solve(M)
>>> print(U.x.values)
[2.0, 8.0]
>>> print(L1.x.values)
[5.5]
>>> print(L2.x.values)
[0.0]
The declarataion of the two lower level problems is naturally contained
within the data of the L1
and L2
objects. Further, the
cross-level interactions are intuitively represented using the index
notation for the objective and constraint data objects.
Note that this more explicit representation clarifies some ambiguity in the expression of lower-levels in the Pyomo representation. The Pyomo representation of PAO2 only specifies the fixed variables that are used in each of the two lower-level problems. PAO analyzes the use of decision variables in Pyomo models, and treats unused variables as fixed. Thus, the Pyomo and MPR representations generate a consistent interpretation of the variable specifications. However, the MPR representation is more explicit in this regard.
Trilevel Problems¶
We consider again the trilevel problem described by Anadalingam (3), which can be expressed as a trilevel linear problem as follows:
>>> M = pao.mpr.LinearMultilevelProblem()
>>> U = M.add_upper(nxR=1)
>>> U.x.lower_bounds = [0]
>>> L = U.add_lower(nxR=1)
>>> L.x.lower_bounds = [0]
>>> B = L.add_lower(nxR=1)
>>> B.x.lower_bounds = [0]
>>> B.x.upper_bounds = [0.5]
>>> U.minimize = True
>>> U.c[U] = [-7]
>>> U.c[L] = [-3]
>>> U.c[B] = [4]
>>> L.minimize = True
>>> L.c[L] = [-1]
>>> B.minimize = True
>>> B.c[B] = [-1]
>>> B.inequalities = True
>>> B.A[U] = [[1], [ 1], [-1], [-1]]
>>> B.A[L] = [[1], [ 1], [-1], [ 1]]
>>> B.A[B] = [[1], [-1], [-1], [ 1]]
>>> B.b = [3,1,-1,1]
Bilinear Problems¶
The QuadraticMultilevelProblem
class can represent general
quadratic problems with quadratic terms in the objective and constraints
at each level. The special case where bilinear terms arise with an
upper-level binary variable multiplied with a lower-level variable is
common in many applications. For this case, PAO provides a function to
linearize these bilinear terms.
We consider again the bilevel problem PAO3 (4), which is represented and solved as follows:
>>> M = pao.mpr.QuadraticMultilevelProblem()
>>> U = M.add_upper(nxR=2, nxB=2)
>>> L = U.add_lower(nxR=1)
>>> U.x.lower_bounds = [2, np.NINF, 0, 0]
>>> U.x.upper_bounds = [6, np.PINF, 1, 1]
>>> L.x.lower_bounds = [0]
>>> L.x.upper_bounds = [np.PINF]
>>> U.c[U] = [1, 0, 5, 0]
>>> U.c[L] = [3]
>>> L.c[L] = [1]
>>> L.maximize = True
>>> U.A[U] = [[ 1, 1, 0, 0],
... [-1, -1, 0, 0],
... [ 0, 0, -1, -1]
... ]
>>> U.b = [10, -10, -1]
>>> L.A[U] = [[ 1, 0, 0, 0],
... [-1, 0, 0, 0],
... [ 1, 0, 0, 0]]
>>> L.A[L] = [[ 0],
... [-4],
... [ 0]]
>>> L.Q[U,L] = (3,4,1), {(0,2,0):1, (2,3,0):2}
>>> L.b = [8, -8, 13]
>>> lmr, soln = pao.mpr.linearize_bilinear_terms(M, 100)
>>> opt = pao.Solver("pao.mpr.FA")
>>> results = opt.solve(lmr)
>>> soln.copy(From=lmr, To=M)
>>> print(U.x.values)
[6.0, 4.0, 0, 1]
>>> print(L.x.values)
[3.5]
The data L.Q[U,L]
specifies the bilinear terms multiplying
variables from level U
with variables from level L
, which
are included in the constraints in level L
. Note that Q
is a
tensor, which is indexed over the constraints, upper-level variables
and lower-level variables. A similar syntax is used to define bilinear
terms in objectives, P
, though that is represented as a sparse matrix.
Quadratic terms can be specified simply by using the same levels to index
Q
or P
.
Model transformations like :func:.linearize_bilinear_terms
are
described in further detail in the next section. Note that this function
returns both the transformed model as well as a helper class that maps
solutions back to the original model. This logic facilitates the
automation of model transformations within PAO.