Pyomo - Optimization Modelling in Python
Pyomo - Optimization Modelling in Python
Pyomo
Optimization Modeling in Python
Third Edition
Springer
Sandia National Laboratories is a multimission laboratory managed and operated by National Technology & Engineering Solutions of Sandia, LLC, a wholly owned
subsidiary of Honeywell International Inc., for the U.S. Department of Energy’s National Nuclear Security Administration under contract DE-NA0003525.
To Pyomo users, past, present, and future.
Preface
This book describes a tool for mathematical modeling: the Python Optimization
Modeling Objects (Pyomo) software. Pyomo supports the formulation and analy-
sis of mathematical models for complex optimization applications. This capability
is commonly associated with algebraic modeling languages (AMLs), which support
the description and analysis of mathematical models with a high-level language. Al-
though most AMLs are implemented in custom modeling languages, Pyomo’s mod-
eling objects are embedded within Python, a full-featured high-level programming
language that contains a rich set of supporting libraries. Pyomo has won awards
from the R&D100 organization and from the INFORMS Computing Society.
Modeling is a fundamental process in many aspects of scientific research, engi-
neering and business, and the widespread availability of computing has made the
numerical analysis of mathematical models a commonplace activity. Furthermore,
AMLs have emerged as a key capability for robustly formulating large models for
complex, real-world applications [37]. AMLs streamline the process of formulating
models by simplifying the management of sparse data and supporting the natural ex-
pression of model components. Additionally, AMLs like Pyomo support scripting
with model objects, which facilitates the custom analysis of complex problems.
The core of Pyomo is an object-oriented capability for representing optimization
models. Pyomo also contains packages that define modeling extensions and model
reformulations. Pyomo also includes packages that define interfaces to solvers like
CPLEX and Gurobi, as well as solver services like NEOS.
vii
viii Preface
Another goal of this book is to illustrate the breadth of Pyomo’s capabilities. Py-
omo supports the formulation and analysis of common optimization models, includ-
ing linear programs, mixed-integer linear programs, nonlinear programs, mixed-
integer nonlinear programs, mathematical programs with equilibrium constraints,
constraints and objectives based on differential equations, generalized disjunctive
programs, among others. Additionally, Pyomo includes solver interfaces for a vari-
ety of widely used optimization software packages, including CBC, CPLEX, GLPK,
and Gurobi. Additionally, Pyomo models can be optimized with optimizers like
IPOPT that employ the AMPL Solver Library interface.
Finally, a goal of this book is to help users get started with Pyomo even if
they have little knowledge of Python. Appendix A provides a quick introduction
to Python, but we have been impressed with how well Python reference texts sup-
port new Pyomo users. Although Pyomo introduces Python objects and a process
for applying them, the expression of models with Pyomo strongly reflects Python’s
clean, concise syntax.
However, our discussion of Pyomo’s advanced modeling capabilities assumes
some background in object-oriented design and features of the Python program-
ming language. For example, our discussion of modeling components distinguishes
between class definitions and class instances. We have not attempted to describe
these advanced features of Python in the book. Thus, a user should expect to develop
some familiarity with Python in order to effectively understand and use advanced
modeling features.
This book provides a reference for students, academic researchers and practitioners.
The design of Pyomo is simple enough that it has been effectively used in the class-
room with undergraduate and graduate students. However, we assume that the reader
is generally familiar with optimization and mathematical modeling. Although this
book does not contain a glossary, we recommend the Mathematical Programming
Glossary [32] as a reference for the reader.
Pyomo is also a valuable tool for academic researchers and practitioners. A key
focus of Pyomo development has been on the ability to support the formulation and
analysis of real-world applications. Consequently, issues like run-time performance
and robust solver interfaces are important.
Additionally, we believe that researchers will find that Pyomo provides an ef-
fective framework for developing high-level optimization and analysis tools. For
example, Pyomo is the basis of a package for optimization under uncertainty called
mpi-sppy, and it leverages the fact that Pyomo’s modeling objects are embedded
within a full-featured high-level programming language. This allows for transpar-
ent parallelization of sub-problems using Python parallel communication libraries.
This ability to support generic solvers for complex models is very powerful, and we
believe that it can be used with many other optimization analysis techniques.
Preface ix
We have made several major changes while preparing the third edition of this book.
A subtle change that permeates the book is in how we recommend that Pyomo be im-
ported. A bigger change that permeates the book is an emphasis on concrete models.
The introductory chapter starts with a concrete model, and we emphasize concrete
models in most chapters other than the chapter devoted entirely to abstract models.
This does not reflect a change in Pyomo’s capabilities, but rather a recognition that
concrete models provide fewer restrictions on the specification and use of Pyomo
models. For example, data can be loaded by the user using general Python utilities
rather than the mechanisms supported specifically for abstract models. Thus, con-
crete models enable a more general discussion of Pyomo’s potential. Finally, we
have reorganized much of the material, added new examples, and added a chapter
on how modelers can improve the performance of their models.
This book documents the capabilities of the Pyomo 6.0 release. Further information
is available on the Pyomo website, including errata:
http://www.pyomo.org
Pyomo’s open source software is hosted at GitHub, and the examples used in this
book are included in the pyomo/examples/doc/pyomobook directory:
https://github.com/Pyomo/pyomo
We encourage feedback from readers, either through direct communication with the
authors or with the Pyomo Forum:
[email protected]
Good luck!
We are grateful for the efforts of many people who have supported current and
previous editions of this book. We thank Elizabeth Loew at Springer for helping
shepherd this book from an initial concept to final production; her enthusiasm for
publishing is contagious. Also, we thank Madelynne Farber at Sandia National Lab-
oratories for her guidance with the legal process for releasing open source software
and book publishing. Finally, we thank Doug Prout for developing the Pyomo, PySP,
Pyomo.DAE, and Coopr logos.
We are indebted to our reviewers for the time and effort they put into helping
this book be successful. Without them, this book would contain many typos and
software bugs. So, thanks to Jack Ingalls, Zev Friedman, Harvey Greenberg, Sean
Legg, Angelica Wong, Daniel Word, Deanna Garcia, Ellis Ozakyol, and Florian
Mader. Special thanks to Amber Gray-Fenner and Randy Brost.
We are particularly grateful to the growing community of Pyomo users. Your
interest and enthusiasm for Pyomo was the most important factor in our decision
to write this book. We thank the early adopters of Pyomo who have provided
detailed feedback on the design and utility of the software, including Fernando
Badilla, Steven Chen, Ned Dmitrov, YueYue Fan, Eric Haung, Allen Holder, An-
dres Iroume, Darryl Melander, Carol Meyers, Pierre Nancel-Penard, Mehul Rang-
wala, Eva Worminghaus and David Alderson. Your feedback continues to have a
major impact on the design and capabilities of Pyomo.
We also thank our friends in the COIN-OR project for supporting the Pyomo
software. Although the main development site for Pyomo is hosted at GitHub, our
partnership with COIN-OR is a key part of our strategy to ensure that Pyomo re-
mains a viable open source software project.
A special thanks goes to our collaborators who have contributed to packages in
Pyomo: Francisco Muñoz, Timothy Ekl, Kevin Hunter, Patrick Steele, and Daniel
Word. We also thank Tom Brounstein, Dave Gay, and Nick Benevidas for helping
develop Python modules and documentation for Pyomo.
The authors gratefully acknowledge this support that contributed to the devel-
opment of this book: National Science Foundation under Grant CBET#0941313
and CBET#0955205, the Office of Advanced Scientific Computing Research within
xi
xii Acknowledgments
the U.S. Department of Energy Office of Science, the U.S. Department of En-
ergy ARPA-E under the Green Electricity Network Integration program, the In-
stitute for the Design of Advanced Energy Systems (IDAES) with funding from
the Simulation-Based Engineering, Crosscutting Research Program within the U.S.
Department of Energy’s Office of Fossil Energy, the U.S. Department of Energy’s
Office of Electricity Advanced Grid Modeling (AGM) program, and the Laboratory
Directed Research and Development program at Sandia National Laboratories
And finally, we would like to thank our families and friends for putting up with
our passion for optimization software.
Disclaimers
ernment or Lawrence Livermore National Security, LLC, and shall not be used for
advertising or product endorsement purposes. This work was performed in part un-
der the auspices of the U.S. Department of Energy by Lawrence Livermore National
Laboratory under Contract DE-AC52-07NA27344.
Contents
1 Introduction 1
1.1 Modeling Languages for Optimization . . . . . . . . . . . . . . . . 1
1.2 Modeling with Pyomo . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2.1 Simple Examples . . . . . . . . . . . . . . . . . . . . . . 3
1.2.2 Graph Coloring Example . . . . . . . . . . . . . . . . . . 5
1.2.3 Key Pyomo Features . . . . . . . . . . . . . . . . . . . . 6
1.3 Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.4 Book Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.5 Discussion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3 Pyomo Overview 27
3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.2 The Warehouse Location Problem . . . . . . . . . . . . . . . . . . 28
3.3 Pyomo Models . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.3.1 Components for Variables, Objectives, and Constraints . . 29
3.3.2 Indexed Components . . . . . . . . . . . . . . . . . . . . 30
3.3.3 Construction Rules . . . . . . . . . . . . . . . . . . . . . 32
3.3.4 A Concrete Model for the Warehouse Location Problem . 33
xiv
Contents xv
Bibliography 225
Index 229
Chapter 1
Introduction
Abstract This chapter introduces and motivates Pyomo, a Python-based tool for
modeling and solving optimization problems. Modeling is a fundamental process in
many aspects of scientific research, engineering, and business. Algebraic modeling
languages like Pyomo are high-level languages for specifying and solving math-
ematical optimization problems. Pyomo is a flexible, extensible modeling frame-
work that captures and extends central ideas found in modern algebraic modeling
languages, all within the context of a widely used programming language.
This book describes a tool for mathematical modeling: the Python Optimization
Modeling Objects (Pyomo) software package. Pyomo supports the formulation and
analysis of mathematical models for complex optimization applications. This ca-
pability is commonly associated with commercial algebraic modeling languages
(AMLs) such as AIMMS [1], AMPL [2], and GAMS [22]. Pyomo implements a
rich set of modeling and analysis capabilities, and it provides access to these ca-
pabilities within Python, a full-featured, high-level programming language with a
large set of supporting libraries.
Optimization models define the goals or objectives for a system under consid-
eration. Optimization models can be used to explore trade-offs between goals and
objectives, identify extreme states and worst-case scenarios, and identify key factors
that influence phenomena in a system. Consequently, optimization models are used
to analyze a wide range of scientific, business, and engineering applications.
The widespread availability of computing resources has made the numerical anal-
ysis of optimization models commonplace. The computational analysis of an opti-
mization model requires the specification of a model that is communicated to a
solver software package. Without a language to specify optimization models, the
process of writing input files, executing a solver, and extracting results from a solver
is tedious and error-prone. This difficulty is compounded in complex, large-scale,
1
2 1 Introduction
real-world applications that are difficult to debug when errors occur. Additionally,
solvers use many different input formats, but few of them are considered to be stan-
dards. Thus, the application of multiple solvers to analyze a single optimization
model introduces additional complexities. Furthermore, model verification (i.e., en-
suring that the model communicated to the solver accurately reflects the model the
developer intended to express) is extremely difficult without high-level languages
for expressing models.
AMLs are high-level languages for describing and solving optimization prob-
lems [26, 37]. AMLs minimize the difficulties associated with analyzing optimiza-
tion models by enabling high-level specification of optimization problems. Further-
more, AML software provides rigorous interfaces to external solver packages that
are used to analyze problems, and it allows the user to interact with solver results in
the context of their high-level model specification.
Custom AMLs like AIMMS [1], AMPL [2, 21], and GAMS [22] implement op-
timization model specification languages with an intuitive and concise syntax for
defining variables, constraints, and objectives. Further, these AMLs support speci-
fication of abstract concepts such as sparse sets, indices, and algebraic expressions,
which are essential when specifying large-scale, real-world problems with thou-
sands or millions of constraints and variables. These AMLs can represent a wide
variety of optimization models, and they interface with a rich set of solver pack-
ages. AMLs are increasingly being extended to include custom scripting capabili-
ties, which enables expression of high-level analysis algorithms concurrently with
optimization model specifications.
A complementary strategy is to use an AML that extends a standard high-
level programming language (as opposed to being based a proprietary language)
to formulate optimization models that are analyzed with solvers written in low-
level languages. This two-language approach leverages the flexibility of the high-
level language for formulating optimization problems and the efficiency of the
low-level language for numerical computations. This is an increasingly common
approach for scientific computing software. The Matlab TOMLAB Optimization
Environment [57] is among the most mature optimization software package using
this approach; Pyomo strongly leverages this approach as well. Similarly, standard
programming languages like Java and C++ have been extended to include AML
constructs. For example, modeling libraries like FlopC++ [19], OptimJ [47], and
JuMP [13] support the specification of optimization models using an object-oriented
design in C++, Java, and Julia, respectively. Although these modeling libraries sacri-
fice some of the intuitive mathematical syntax of a custom AML, they allow the user
to leverage the flexibility of modern high-level programming languages. A further
advantage of these AML libraries is that they can link directly to high-performance
optimization libraries and solvers, which can be an important consideration in some
applications.
1.2 Modeling with Pyomo 3
min x1 + 2x2
s.t. 3x1 + 4x2 ≥ 1
2x1 + 5x2 ≥ 2
x1 , x2 ≥ 0
model = pyo.ConcreteModel()
model.x_1 = pyo.Var(within=pyo.NonNegativeReals)
model.x_2 = pyo.Var(within=pyo.NonNegativeReals)
model.obj = pyo.Objective(expr=model.x_1 + 2*model.x_2)
model.con1 = pyo.Constraint(expr=3*model.x_1 + 4*model.x_2 >= 1)
model.con2 = pyo.Constraint(expr=2*model.x_1 + 5*model.x_2 >= 2)
The first line is a standard Python import statement that initializes the Pyomo envi-
ronment and loads Pyomo’s core modeling component library. The next lines con-
struct a model object and define model attributes. This example describes a concrete
model. Model components are objects that are attributes of a model object, and the
ConcreteModel object initializes each model component as they are added. The
model decision variables, constraints, and objective are defined using Pyomo model
components.
Users rarely have a single instance of a particular optimization problem to solve.
Rather, they commonly have a general optimization model and then create a particu-
lar instance of that model using specific data. For example, the following equations
represent an LP with scalar parameters n and m, vector parameters b and c, and
matrix parameter a:
min ∑ni=1 ci xi
s.t. ∑ni=1 a ji xi ≥ b j ∀ j = 1 . . . m
xi ≥ 0 ∀i = 1 . . . n
4 1 Introduction
model = pyo.ConcreteModel()
def obj_rule(model):
return sum(mydata.c[i]*model.x[i] for i in mydata.N)
model.obj = pyo.Objective(rule=obj_rule)
This script requires that the data used to construct the model is available while each
modeling component is constructed. In this example, the necessary data exists in
mydata.py:
N = [1,2]
M = [1,2]
c = {1:1, 2:2}
a = {(1,1):3, (1,2):4, (2,1):2, (2,2):5}
b = {1:1, 2:2}
model = pyo.AbstractModel()
model.N = pyo.Set()
model.M = pyo.Set()
model.c = pyo.Param(model.N)
model.a = pyo.Param(model.M, model.N)
model.b = pyo.Param(model.M)
def obj_rule(model):
return sum(model.c[i]*model.x[i] for i in model.N)
model.obj = pyo.Objective(rule=obj_rule)
This example includes model components that provide abstract or symbolic defi-
nitions of set and parameter values. The AbstractModel object defers initial-
1.2 Modeling with Pyomo 5
param : M : b :=
1 1
2 2 ;
param a :=
1 1 3
1 2 4
2 1 2
2 2 5 ;
the number of colors used in a solution. The fourth and final constraint enforces the
binary constraint on xv,c .
Figure 1.1 shows a Pyomo specification of the above graph coloring formulation,
using a concrete model; the example is adapted from Gross and Yellen [27]. This
specification consists of Python commands that define a ConcreteModel object,
and then define various attributes of this object, including variables, constraints, and
the optimization objective. Lines 10–24 define the model data. Line 28 is a standard
Python import statement that adds all of the symbols (e.g., classes and functions)
defined in pyomo.environ to the current Python namespace. Line 31 specifies
creation of the model object, which is an instance of the ConcreteModel class.
Lines 34 and 35 define the model decision variables. Note that y is a scalar variable,
while x is a two-dimensional array of variables. The remaining lines in the example
define the model constraints and objective. The Objective class defines a single
optimization objective using the expr keyword option. The ConstraintList
class defines a list of constraints, which are individually added.
When compared to custom AMLs, Pyomo models are clearly more verbose (e.g.,
see Hart et al. [30]). However, this example illustrates how Python’s clean syntax
still allows Pyomo to express mathematical concepts intuitively and concisely. Aside
from the use of Pyomo classes, this example employs standard Python syntax and
methods. For example, line 41 uses Python’s generator syntax to iterate over all ele-
ments of the colors set and apply the Python sum function to the result. Although
Pyomo does include some utility functions to simplify the construction of expres-
sions, Pyomo does not rely on sophisticated extensions of core Python functionality.
Python
Customizable Capability
1 #
2 # Graph c o l o r i n g e x a m p l e a d a p t e d from
3 #
4 # J o n a t h a n L . G r o s s and J a y Y e l l e n ,
5 # ” Graph T h e o r y and I t s A p p l i c a t i o n s , 2 nd E d i t i o n ” ,
6 # Chapman & H a l l / CRC, Boca Raon , FL , 2 0 0 6 .
7 #
8
9 # Define data f o r the graph of i n t e r e s t .
10 v e r t i c e s = s e t ( [ ’ Ar ’ , ’Bo ’ , ’ Br ’ , ’Ch ’ , ’Co ’ , ’ Ec ’ ,
11 ’FG ’ , ’Gu ’ , ’ Pa ’ , ’ Pe ’ , ’ Su ’ , ’ Ur ’ , ’Ve ’ ] )
12
13 e d g e s = s e t ( [ ( ’ FG ’ , ’ Su ’ ) , ( ’ FG ’ , ’ Br ’ ) , ( ’ Su ’ , ’ Gu ’ ) ,
14 ( ’ Su ’ , ’ Br ’ ) , ( ’ Gu ’ , ’ Ve ’ ) , ( ’ Gu ’ , ’ Br ’ ) ,
15 ( ’ Ve ’ , ’ Co ’ ) , ( ’ Ve ’ , ’ Br ’ ) , ( ’ Co ’ , ’ Ec ’ ) ,
16 ( ’ Co ’ , ’ Pe ’ ) , ( ’ Co ’ , ’ Br ’ ) , ( ’ Ec ’ , ’ Pe ’ ) ,
17 ( ’ Pe ’ , ’ Ch ’ ) , ( ’ Pe ’ , ’ Bo ’ ) , ( ’ Pe ’ , ’ Br ’ ) ,
18 ( ’ Ch ’ , ’ Ar ’ ) , ( ’ Ch ’ , ’ Bo ’ ) , ( ’ Ar ’ , ’ Ur ’ ) ,
19 ( ’ Ar ’ , ’ Br ’ ) , ( ’ Ar ’ , ’ Pa ’ ) , ( ’ Ar ’ , ’ Bo ’ ) ,
20 ( ’ Ur ’ , ’ Br ’ ) , ( ’ Bo ’ , ’ Pa ’ ) , ( ’ Bo ’ , ’ Br ’ ) ,
21 ( ’ Pa ’ , ’ Br ’ ) ])
22
23 ncolors = 4
24 c o l o r s = range ( 1 , n c o l o r s +1)
25
26
27 # Python import s t a t e m e n t
28 i m p o r t pyomo . e n v i r o n a s pyo
29
30 # C r e a t e a Pyomo model o b j e c t
31 model = pyo . C o n c r e t e M o d e l ( )
32
33 # D e f i n e model v a r i a b l e s
34 model . x = pyo . Var ( v e r t i c e s , c o l o r s , w i t h i n =pyo . B i n a r y )
35 model . y = pyo . Var ( )
36
37 # Each node i s c o l o r e d w i t h one c o l o r
38 model . n o d e c o l o r i n g = pyo . C o n s t r a i n t L i s t ( )
39 for v in v e r t i c e s :
40 model . n o d e c o l o r i n g . add (
41 sum ( model . x [ v , c ] f o r c i n c o l o r s ) == 1 )
42
43 # Nodes t h a t s h a r e an e d g e c a n n o t be c o l o r e d t h e same
44 model . e d g e c o l o r i n g = pyo . C o n s t r a i n t L i s t ( )
45 f o r v ,w in edges :
46 for c in colors :
47 model . e d g e c o l o r i n g . add (
48 model . x [ v , c ] + model . x [w, c ] <= 1 )
49
50 # P r o v i d e a l o w e r bound on t h e minimum number o f c o l o r s
51 # t h a t a r e needed
52 model . m i n c o l o r i n g = pyo . C o n s t r a i n t L i s t ( )
53 for v in v e r t i c e s :
54 for c in colors :
55 model . m i n c o l o r i n g . add (
56 model . y >= c ∗ model . x [ v , c ] )
57
58 # M i n i m i z e t h e number o f c o l o r s t h a t a r e n e e d e d
59 model . o b j = pyo . O b j e c t i v e ( e x p r = model . y )
Fig. 1.1: A concrete Pyomo model for the minimum graph coloring problem.
8 1 Introduction
solvers, and solver managers. A plug-in framework manages the registration of these
capabilities. Thus, users can customize Pyomo in a modular manner without the risk
of destabilizing core functionality.
Pyomo models can be analyzed both using command-line tools and via Python
scripts. The pyomo command line utility provides a generic interface to most Py-
omo modeling capabilities. The pyomo command supports a generic optimization
process. This process can easily be replicated in a Python script and further cus-
tomized for a user’s specific needs.
The examples in Section 1.2.1 illustrate Pyomo’s support for both concrete and ab-
stract model definitions. The difference between these modeling approaches relates
to when modeling components are initialized: concrete models immediately initial-
ize components, and abstract models delay the initialization of components until
a later model initialization action. Consequently, these modeling approaches are
equivalent, and the choice of approach depends on the context in which Pyomo is
used and user preference. Both types of models can be easily initialized with data
from a wide range of data sources (e.g., csv, json, yaml, excel, and databases).
Object-Oriented Design
Pyomo employs an object-oriented library design. Models are Python objects, and
model components are attributes of these models. This design allows Pyomo to au-
tomatically manage the naming of modeling components, and it naturally segregates
modeling components within different model objects. Pyomo models can be further
structured with blocks, which support a hierarchical nesting of model components.
Many of Pyomo’s advanced modeling features leverage this structured modeling
capability.
Solver Integration
Pyomo supports both tightly and loosely coupled solver interfaces. Tightly cou-
pled modeling tools directly access optimization solver libraries (e.g., via static
or dynamic linking), and loosely coupled modeling tools apply external optimiza-
tion executables (e.g., through the use of system calls). Many optimization solvers
read problems from well-known data formats (e.g., the AMPL nl format [24]);
these solvers are loosely coupled with Pyomo. Solvers with Python interfaces (e.g.,
Gurobi and CPLEX) can be tightly coupled, which avoids writing external files.
Open Source
In order to execute all of the examples in this book the following software should
be installed:
• Python 3.6 or higher (although almost all examples will work with earlier ver-
sions of Python). Pyomo currently relies on CPython; there is only support for
Jython and PyPy for a subset of Pyomo’s capability.
• Pyomo 6.0, which is used throughout this book.
• The GLPK [25] solver, which is used to generate output for most examples in
this book. Other LP and MILP solvers can be used for these examples, but the
GLPK software is easily installed and widely available.
• The IPOPT [34] solver, which is used to generate output for nonlinear model
examples. Other nonlinear optimizers can be easily used for these examples if
they are compiled with the AMPL Solver Library [23].
10 1 Introduction
• The CPLEX [11] solver, which is used to generate output for stochastic pro-
gramming examples. This commercial solver provides capabilities needed for
these examples that are not commonly available in open source optimization
solvers (e.g., optimization of quadratic integer programs).
• The matplotlib Python package, which is used to generate plots.
Installation instructions for Pyomo are provided at the Pyomo website:
www.pyomo.org. Appendix A provides a brief tutorial of the Python scripting
language; various on-line sources provide more comprehensive tutorials and docu-
mentation.
The remainder of this book is divided into three parts. The first part provides an
introduction to Pyomo. Chapter 2 provides a primer on optimization and mathemat-
ical modeling, including brief illustrations of how Pyomo can be used to specify and
solve algebraic optimization models. Chapter 3 illustrates Pyomo’s modeling capa-
bilities with simple concrete and abstract models, and Chapter 4 describes Pyomo’s
core modeling components. The basics of embedding Pyomo models in scripts is in
Chapter 5. The first part closes with Chapter 6 describing interaction with solvers.
The second part of this book documents advanced features and extensions. Chap-
ter 7 describes the nonlinear programming capabilities of Pyomo, and Chapter 8 de-
scribes how hierarchical models can be expressed in Pyomo. Guidance on improv-
ing performance is given in Chapter 9. Chapter 10 describes the AbstractModel
class, the syntax of Pyomo data command files, and Pyomo’s command-line inter-
face.
The third part of the book describes some of the modeling extensions. An
overview of generalized disjunctive programming is provided in Chapter 11. Dy-
namic models expressed with differential and algebraic equations are described in
Chapter 12, and programs with equilibrium constraints are described in Chapter 13).
NOTE: This book does not provide a complete reference for Pyomo. Instead,
our goal is to discuss core functionality that is available in the Pyomo 6.0 re-
lease.
1.5 Discussion
A variety of developers have realized that Python’s clean syntax and rich set of
supporting libraries make it an excellent choice for optimization modeling [30]. A
variety of open source software packages provide optimization modeling capabil-
ities in Python, such as PuLP [49], APLEpy [4], and OpenOpt [46]. Additionally,
1.5 Discussion 11
there are many Python-based solver interface packages, including open source pack-
ages such as PyGlpk [50] and pyipopt [51], in addition to Python interfaces for the
commercial solvers such as CPLEX [11] and Gurobi [28].
Several features distinguish Pyomo. First, Pyomo provides mechanisms for ex-
tending the core modeling and optimization functionality without requiring edits to
Pyomo itself. Second, Pyomo supports the definition of both concrete and abstract
models. This allows the user significant flexibility in determining how closely data is
integrated with a model definition. Finally, Pyomo can support a broad class of op-
timization models, including both standard linear programs as well as general non-
linear optimization models, generalized disjunctive programs, problems constrained
by differential equations, and mathematical programs with equilibrium conditions.
Part I
An Introduction to Pyomo
Chapter 2
Mathematical Modeling and Optimization
2.1.1 Overview
15
16 2 Mathematical Modeling and Optimization
A Model, in the sense that we will use the word, represents items by abstracting
away some features. Everyone is familiar with physical models, such as model rail-
roads or model cars. Our interest is in mathematical models that use symbols to
represent aspects of a system or real-world object.
For example, a person might want to determine the best number of scoops of ice
cream to buy. We could use the symbol x to represent the number of scoops. We
might use c to represent the cost per scoop. So then we could model the total cost as
c times x, which we usually write as cx.
We might need a more sophisticated model of total cost if there are volume dis-
counts or surcharges for buying fractional scoops. Also, this model is probably not
valid for negative values of x. It is seldom possible to sell back ice cream for the
same price paid for it.
It is more complicated to provide a mathematical model of the happiness associ-
ated with scoops of ice cream on an ice cream cone. One approach is to use a scaled
measure of happiness. We will do that using the basic unit of the happiness asso-
ciated with one scoop of ice cream, which we call h. A simple model, then, would
be to say that the total happiness from x scoops of ice cream is h times x, which we
write as hx. For some people, that might be a pretty good approximation for values
of x between one-half and three, but there is almost no one who is 100 times as
happy to have 100 scoops of ice cream on their ice cream cone as they are to have
one scoop. For some people, the model of happiness for values of x between zero
and ten might be something like
h · x − (x/5)2 .
Note that this model becomes negative when there are more than 25 scoops on the
cone, which might not be a good model for everyone.
2.1 Mathematical Modeling 17
It is common to want to model more than one thing at a time. For example,
you might be able to have scoops of ice cream and peanuts. Since there are multiple
things that can be purchased, we can represent the quantity purchased using a vector
x (i.e. the symbol x now represents a list). We refer to elements of the list using the
notation xi where the symbol i indexes the vector. For example, if we agree that
the first element is the number of scoops of ice cream, then this number could be
referenced using x1 . For higher dimensions a tuple is used, such as i, j or (i, j) as
the index.
Let’s change c to be a vector of costs with the same indices as x (i.e., c1 is the
cost per scoop of ice cream and c2 is the cost per cup of peanuts). So now, we write
the total cost of ice cream and peanuts as
2
c1 x1 + c2 x2 = ∑ ci xi .
i=1
Once again, this cost model is probably not valid for all possible values of all ele-
ments of x, but it might be good enough for some purposes.
Often, it is useful to refer to indices as being members of a set. For the example
just given, we could use the set {1, 2} to write the total cost as
∑ ci xi .
i∈{1,2}
∑ ci xi
i∈A
where the set A is understood to be the index set for c and x (and for our example
the set A would be {1, 2}.)
In addition to summing over an index set, we might want to have conditions that
hold for all members of an index set. This is done simply by using a comma. For
example, if we want to require that none of the values of x can be negative, we would
write
xi ≥ 0, i ∈ A
and we read this line out loud as “x subscript i is greater than or equal to zero for all
i in A.”
There is no law of mathematics or even mathematical modeling requiring the use
of single letter symbols such as x and c or i. It would be perfectly okay for the set A
to be composed of a picture of an ice cream cone and a picture of a cup of peanuts,
but that is hard to work with in some settings. The set could also be {Scoops,Cups},
but that is not commonly done in books because it takes up too much space and
causes lines to overflow. Also, x could be replaced by something like Quantity. Long
names are, importantly, supported by modeling languages such as Pyomo, and it is
generally a good idea to use meaningful names when writing Pyomo models. Spaces
or dashes embedded in names often cause troubles and confusion, so underscores
18 2 Mathematical Modeling and Optimization
2.2 Optimization
where h is given as data. (It turns out not to matter what value of h is given for the
purpose of finding the x that maximizes happiness in this particular example.) The
optimization problem, as we have modeled it, is given as
max h · x − (x/5)2 ,
max h · x − (x/5)2
x
to make it clear x is the decision variable. In this case, there is only one best value of
x, which can be found using numerical optimization. The best value of x turns out
to be fractional, which means that it is not an integer number of scoops. This model
might not be considered useful for a typical ice cream shop, where the number
of scoops must be a non-negative integer. To specify this requirement, we add a
2.2 Optimization 19
max h · x − (x/5)2
x
s.t.
x ∈ non-negative integers
where “s.t.” is an abbreviation for either “subject to” or “such that.” Suppose the
model is not being used in an ice cream shop, but rather at home, where the ice
cream is being served by the model user’s parent. If the parent is willing to make
partial scoops but not willing to go above two scoops, then the constraint
x ∈ non-negative integers
hi · xi − (xi /di )2 ,
where h and d are both data vectors with the same index set as x. Further, let c be a
vector of costs and u be a vector of the most of any product that can be purchased.
Assume that all products can be purchased in fractional quantities for the moment.
Finally, suppose there is a total budget given by b. The optimization problem would
be written as:
We now consider different strategies for formulating and optimizing algebraic opti-
mization models using Pyomo. Although a detailed explanation of Pyomo models is
deferred to Chapter 3, the following examples illustrate the use of Pyomo for model
(H).
A concrete Pyomo model initializes components as they are constructed. This allows
modelers to easily make use of native Python data structures when defining a model
instance. There are many ways to implement our model as a concrete Pyomo model,
and we start with one using Python lists and dictionaries.
NOTE: Recognizing that we will often make new instances of the model with
different data, we choose to write a Python function that takes in the required
data as arguments and returns a Pyomo model. Using this approach, enables us
to reuse the general Pyomo model with different definitions of the data.
def z_rule(model):
return sum(h[i] * (model.x[i] - (model.x[i]/d[i])**2)
for i in A)
model.z = pyo.Objective(rule=z_rule, sense=pyo.maximize)
model.budgetconstr = pyo.Constraint(\
expr = sum(c[i]*model.x[i] for i in A) <= b)
return model
2.3 Modeling with Pyomo 21
In the budgetconstr declaration, we define the constraint directly with the expr
keyword argument, however, a construction rule could also be used. There are more
elegant ways to create this function, but this works well for our purposes.
NOTE: The backslash character at the end of a line tells Python that the line
continues; we use it to help make the lines fit on a book page. In this particular
case it is not strictly required because the line is breaking inside a parenthetical
grouping.
With particular data in hand, one can write a Python program that provides the
data to the function to obtain the fully instantiated Pyomo model. If there is a solver
installed on the computer, the Python program can then send the model to a solver
to be solved and, if successful, query the model for the solution. But before we look
into these steps, let’s consider more of the many ways this function could be written.
Note the function IC model is just a Python function. One could have written
the Pyomo model directly in the script, or defined a function IC model to accept
a dictionary as an argument instead of the more explicit argument list. Python pro-
grammers can probably think of more, and better, ways to write this Python code.
In addition to the modeling components already discussed, Pyomo also of-
fers sets (Set) and parameters (Param), which are components that will be dis-
cussed in subsequent chapters. In the code below, we have defined the function
IC model dict that takes in a Python dictionary and makes use of Set and
Param objects to define the same model.
22 2 Mathematical Modeling and Optimization
def IC_model_dict(ICD):
# ICD is a dictionary with the data for the problem
model.A = pyo.Set(initialize=ICD["A"])
def obj_rule(model):
return sum(model.h[i] * \
(model.x[i] - (model.x[i]/model.d[i])**2)\
for i in model.A)
model.z = pyo.Objective(rule=obj_rule,sense=pyo.maximize)
def budget_rule(model):
return sum(model.c[i]*model.x[i]\
for i in model.A) <= model.b
model.budgetconstr = pyo.Constraint(rule=budget_rule)
return model
2.4.1 Definition
∑i∈A ci xi
∑i∈A xi
x2
c3 x2 + c2 x3
c3 x2 + c2 x3 + 4
2.5 Solving the Pyomo Model 23
On the other hand, the following expressions are not linear: xi2 , x2 x3 and cosine(x2 ).
Linear expressions often result in problems that can be solved with much less
computational effort than similar models with nonlinear expressions. Consequently,
many modelers make an effort to use linear expressions as much as possible, and
some modelers strive to use only linear expressions. Additionally, many model-
ers develop linear approximations to nonlinear models in hopes of finding “good
enough” solutions to the original nonlinear model.
For illustrative purposes, let’s assume we have the following linear approxima-
tion to (H), and we will replace the objective function in (H) with
max ∑ hi · 1 − ui /di2 xi ,
(2.1)
x
i∈A
where ui is a new model parameter. We say that this expression is linear because the
decision variables are only multiplied by data, and summed. It is true the parameter
d is squared, but this is not a decision variable. The numerical value of the entire
expression
hi · 1 − ui /di2
is computed by Pyomo before the problem instance is passed to a solver, and the
task of the solver is to find optimal values for the decision variables.
If we want to modify the concrete model given on page 20 to use expression (2.1),
we would change the objective function expression rule as follows:
def obj_rule(model):
return sum(h[i]*(1 - u[i]/d[i]**2) * model.x[i] \
for i in A)
Pyomo provides automated methods to (1) combine the model and data, (2) send
the resulting model instance to a solver, and (3) recover the results for display and
further use. Pyomo does not, itself, solve optimization problem instances. They are
always passed to a solver of some sort.
24 2 Mathematical Modeling and Optimization
2.5.1 Solvers
Pyomo can be installed without any solvers. For example, Pyomo can simply write
out problem instances into files suitable as direct input to a solver. This use of Pyomo
might be necessary if the solver is run separately on a different computer. Typically,
a solver should be installed and accessible to Pyomo, and most of the examples in
this book make this assumption.
Recall the objective in (H) is not a linear function of the variable, x, and the
budget constraint is linear. Although many solvers can solve an instance with a
quadratic objective and linear constraints, some solvers cannot. If the only solver
on the computer is limited to linear problems, then (H) would need to approximate
with a linear model.
A Python script is executed using Python from the command line or within a de-
velopment environment. As with expressing the model, there are many options for
writing a script to supply its data and to solve it described in subsequent chapters.
For example, a script defining the concrete model given on page 2.3.1 can be created
by adding the following lines:
A = [’I_C_Scoops’, ’Peanuts’]
h = {’I_C_Scoops’: 1, ’Peanuts’: 0.1}
d = {’I_C_Scoops’: 5, ’Peanuts’: 27}
c = {’I_C_Scoops’: 3.14, ’Peanuts’: 0.2718}
b = 12
u = {’I_C_Scoops’: 100, ’Peanuts’: 40.6}
model = IC_model_linear(A, h, d, c, b, u)
opt = pyo.SolverFactory(’glpk’)
results = opt.solve(model) # solves and updates model
pyo.assert_optimal_termination(results)
model.display()
If the resulting file is called ConcHLinScript.py, then it can be run from the
terminal with the command line:
python ConcHLinScript.py
The first few lines that assign data to Python variables look a little strange, because
they are. Usually, data for optimization is read from files or databases; however, for
this textbook example we assign the data using Python literals so it is self-contained.
The last few lines create a solver, solve the model, and display the model with the
solution values. The function assert optimal termination halts the script
and outputs a message if the solver does not report it found an optimal solution. The
2.5 Solving the Pyomo Model 25
Abstract This chapter provides an overview of the modeling strategies and capabil-
ities of Pyomo. A brief discussion of the core modeling components supported by
Pyomo, and some of the modeling capabilities within Pyomo (e.g., discrete variables
and nonlinear models) are provided.
3.1 Introduction
27
28 3 Pyomo Overview
The warehouse location problem is used throughout this chapter. This formulation
seeks to find the locations for a set of warehouses that meet delivery demands while
optimizing transportation costs. Let N be a set of candidate warehouse locations,
and let M be a set of customer locations. For each warehouse n, the cost of deliv-
ering product to customer m is given by dn,m . The goal is to determine the optimal
warehouse locations that will minimize the total cost of product delivery. The binary
variables yn are used to define whether or not a warehouse should be built, where
yn is 1 if warehouse n is selected and 0 otherwise. The variable xn,m indicates the
fraction of demand for customer m that is served by warehouse n.
The variables x and y are to be determined by the optimization solver, while all
other quantities are known inputs or parameters in the problem. This problem is a
particular description of the p-median problem, and it has the interesting property
that there will be optimal x values in {0, 1} even though they are not specified as
binary variables.
The complete problem formulation is:
∑ yn ≤ P (WL.4)
n∈N
0≤x≤1 (WL.5)
y ∈ {0, 1} (WL.6)
Here, the objective (equation WL.1) is to minimize the total cost associated with de-
livering products to all the customers. Equation WL.2 ensures that each customer’s
demand is fully met, and equation WL.3 ensures that a warehouse can deliver prod-
uct to customers only if that warehouse is selected to be built. With equation WL.4
the number of warehouses that can be built is limited to P.
3.3 Pyomo Models 29
For our example, we will assume that P=2, with the following data for warehouse
and customer locations,
Customer locations M = {‘NYC’, ‘LA’, ‘Chicago’, ‘Houston’}
Candidate warehouse locations N = {‘Harlingen’, ‘Memphis’, ‘Ashland’}
with the costs dn,m as given in the following table:
NYC LA Chicago Houston
Harlingen 1956 1606 1410 330
Memphis 1096 1792 531 567
Ashland 485 2322 324 1236
Optimization problems require, at least, one variable and an objective function. Most
problems also include constraints. The Pyomo classes for implementing these mod-
eling components are Var, Objective, and Constraint. The following exam-
ple shows how these components could be defined:
model.x = pyo.Var()
model.y = pyo.Var(bounds=(-2,4))
model.z = pyo.Var(initialize=1.0, within=pyo.NonNegativeReals)
This example includes three optimization variables (x, y, and z), a single objective,
and two constraints. For each optimization variable, an instance of the Var class
is created and that instance is added as an attribute to the model object. The code
model.x=pyo.Var() creates an instance of the Pyomo class Var and assigns
it to model.x. The model object identifies when a component is being added
and performs special processing that includes, for example, setting the name of the
instance of Var to “x”, and setting a reference to the owning model.
This example declares x as a continuous variable, but keyword arguments can
be used to define different properties of the Pyomo Var. For example, bounds is
30 3 Pyomo Overview
used to set lower and upper bounds, initialize is used to set initial values, and
within is used to set the domain. In this example, model.y has a lower bound of
−2 and an upper bound of 4, and model.z has a lower bound of 0, and no upper
bound since the keyword argument within is set to non-negative reals.
NOTE: The use of keyword arguments is common in the constructors for Py-
omo components to specify component properties. See Chapter 4 for more de-
tails about supported keyword arguments for Pyomo components.
NOTE: In the previous example, the objective and constraints were defined
with the expr keyword. While this is convenient for illustrating the examples
with few lines of code, these components are often defined using construction
rules, which are discussed in more detail in Sections 3.3.3 and 4.2.1.
In the previous example, each of the modeling components was scalar. Specifically,
each of the optimization variables x, y, and z were single values only, not vectors
or arrays. The constraints were also scalar, where each declaration created only a
single mathematical constraint. When modeling large, complex applications, it is
common to have vectors of variables and constraints whose dimension and index-
ing is determined according to model data. This is handled within Pyomo through
indexed components.
To illustrate the concept of an indexed component, consider the warehouse loca-
tion problem (WL) defined using only scalar components. Note, a better approach
using indexed components will be shown subsequently. For example, separate x
variables could be created for each pair of warehouses and customers,
3.3 Pyomo Models 31
model.x_Harlingen_NYC = pyo.Var(bounds=(0,1))
model.x_Harlingen_LA = pyo.Var(bounds=(0,1))
model.x_Harlingen_Chicago = pyo.Var(bounds=(0,1))
model.x_Harlingen_Houston = pyo.Var(bounds=(0,1))
model.x_Memphis_NYC = pyo.Var(bounds=(0,1))
model.x_Memphis_LA = pyo.Var(bounds=(0,1))
#...
and, all the constraints in equation (WL.2) could them be explicitly written as,
model.one_warehouse_for_NYC = \
pyo.Constraint(expr=model.x_Harlingen_NYC + \
model.x_Memphis_NYC + model.x_Ashland_NYC == 1)
model.one_warehouse_for_LA = \
pyo.Constraint(expr=model.x_Harlingen_LA + \
model.x_Memphis_LA + model.x_Ashland_LA == 1)
#...
However, this would become very cumbersome for large data sets, and this is
much easier to formulate using indexed components. First, we can define a list of
valid indices for the warehouse locations and the customer locations,
N = [’Harlingen’, ’Memphis’, ’Ashland’]
M = [’NYC’, ’LA’, ’Chicago’, ’Houston’]
We refer to N and M as index sets for the indexed variables model.x and model.y.
Specifically, the variable y is indexed over N, and the variable x is a two-dimensional
array that is indexed over both N and M. With this declaration, an element of x can
be accessed by model.x[i,j] where i and j are elements of the sets N and M,
respectively.
NOTE: Pyomo modeling components can include any number of index sets as
unnamed arguments in their declaration but they must be specified before any
other named keyword arguments. These index sets specify the valid indices for
individual elements of the component.
This declaration uses Python’s iteration syntax to sum over a set of indexed vari-
ables. The list comprehension syntax enables a concise specification of the sum-
32 3 Pyomo Overview
mation, where the syntax specifies that the terms model.y[n] are generated by
iterating over the set N. As these terms are generated, the function sum adds them
together to form the overall expression. Similarly, the objective can be defined as
model.obj = pyo.Objective(expr=sum(d[n,m]*model.x[n,m] for n in \
N for m in M))
∑ xn,m = 1, ∀m∈M
n∈N
This mathematical notation indicates there is a single constraint defined for each
m in the set M. The Constraint component can be declared as an indexed con-
straint over the elements in this set. However, a mechanism is needed to provide
Pyomo with the explicit expressions for each element in M. Pyomo allows model
components to be initialized with user-defined functions called rules.
The following example illustrates the use of a construction rule to define con-
straint (WL.2):
def demand_rule(mdl, m):
return sum(mdl.x[n,m] for n in N) == 1
model.demand = pyo.Constraint(M, rule=demand_rule)
The first two lines define a Python function that will be called to produce the correct
constraint expression for each element in M. The last line in this example declares the
constraint by creating a Constraint component that is indexed over the set de-
fined by M. The rule keyword argument indicates that the function demand rule
will be called to construct each constraint.
The first argument in the function demand rule will automatically be set to
the instance of the model object being constructed. It is followed by arguments
providing the indices of the particular constraint being constructed. When Pyomo
constructs the Constraint object, the construction rule is called once for each of
the values of the specified index sets.
Construction rules can be used for most modeling components, using the rule
3.3 Pyomo Models 33
keyword argument, even if the component is not indexed. Although the function
arguments for component rules are the same for all component types, the following
table illustrates that the expected type of the return value is different:
1 # wl_concrete.py
2 # ConcreteModel version of warehouse location problem
3 import pyomo.environ as pyo
4
5 def create_warehouse_model(N, M, d, P):
6 model = pyo.ConcreteModel(name="(WL)")
7
8 model.x = pyo.Var(N, M, bounds=(0,1))
9 model.y = pyo.Var(N, within=pyo.Binary)
10
11 def obj_rule(mdl):
12 return sum(d[n,m]*mdl.x[n,m] for n in N for m in M)
13 model.obj = pyo.Objective(rule=obj_rule)
14
15 def demand_rule(mdl, m):
16 return sum(mdl.x[n,m] for n in N) == 1
17 model.demand = pyo.Constraint(M, rule=demand_rule)
18
19 def warehouse_active_rule(mdl, n, m):
20 return mdl.x[n,m] <= mdl.y[n]
21 model.warehouse_active = pyo.Constraint(N, M, \
rule=warehouse_active_rule)
22
23 def num_warehouses_rule(mdl):
24 return sum(mdl.y[n] for n in N) <= P
25 model.num_warehouses = \
pyo.Constraint(rule=num_warehouses_rule)
26
27 return model
This file begins by importing the Pyomo environment, which defines the Python
classes used to build a model. Line 5 defines a function that will be called to create
and return the model. This is not necessary, and the model can be created directly in
34 3 Pyomo Overview
a Python script, however, this strategy is often preferred so this model construction
code can be easily reused with different data. Line 6 creates the ConcreteModel
and provides a name.
Lines 8 and 9 declare and construct the variables for the problem. The model
object is a ConcreteModel, and once these lines are executed, the variables x
and y are completely constructed with known indices. Lines 11 and 12 define the
construction rule for the objective function, and line 13 declares the objective func-
tion and assigns it to model.obj. As soon as line 13 executes, the rule declared
on lines 11 and 12 is called to construct the expression for the objective function.
Similarly, in the remaining lines of the Python file, the constraint rules are declared,
followed by the constraint objects themselves. Since this is a ConcreteModel,
the constraint rules are called when Python executes lines 17, 21, and 25. Line 27
returns the constructed model from the function.
Now that the model is defined, we can create a short Python script that solves a
particular instance of the model and shows the solution.
1 # wl_concrete_script.py
2 # Solve an instance of the warehouse location problem
3
4 # Import Pyomo environment and model
5 import pyomo.environ as pyo
6 from wl_concrete import create_warehouse_model
7
8 # Establish the data for this model (this could also be
9 # imported using other Python packages)
10
11 N = [’Harlingen’, ’Memphis’, ’Ashland’]
12 M = [’NYC’, ’LA’, ’Chicago’, ’Houston’]
13
14 d = {(’Harlingen’, ’NYC’): 1956, \
15 (’Harlingen’, ’LA’): 1606, \
16 (’Harlingen’, ’Chicago’): 1410, \
17 (’Harlingen’, ’Houston’): 330, \
18 (’Memphis’, ’NYC’): 1096, \
19 (’Memphis’, ’LA’): 1792, \
20 (’Memphis’, ’Chicago’): 531, \
21 (’Memphis’, ’Houston’): 567, \
22 (’Ashland’, ’NYC’): 485, \
23 (’Ashland’, ’LA’): 2322, \
24 (’Ashland’, ’Chicago’): 324, \
25 (’Ashland’, ’Houston’): 1236 }
26 P = 2
27
28 # Create the Pyomo model
29 model = create_warehouse_model(N, M, d, P)
30
31 # Create the solver interface and solve the model
32 solver = pyo.SolverFactory(’glpk’)
33 res = solver.solve(model)
3.3 Pyomo Models 35
34 pyo.assert_optimal_termination(res)
35
36 model.y.pprint() # Print the optimal warehouse locations
Line 5 imports the Pyomo environment, and line 6 imports the function defined in
wl concrete.py to create the model from the passed in data. Lines 11 through
26 define the data for this problem. The Python lists N and M are used to specify
the valid warehouse locations and the customer locations respectively. The Python
dictionary d defines the costs associated with serving each customer from each lo-
cation, and line 26 specifies P, providing the number of warehouses needed.
In line 29 these native Python data structures are passed to the function written,
create warehouse location, where they are used to declare and construct
the Pyomo modeling components, the Var, Objective, and Constraint ob-
jects. The constructed model is returned from the function and assigned to model.
Line 32 creates an interface to the solver “glpk” that can be used to solve the opti-
mization problem. Line 33 calls solve to execute the solver and return a results ob-
ject to res, which is passed to the function assert optimal termination in
line 34. If the solver does not report an optimal solution (possibly because the solver
is not properly installed or because there is not an optimal solution), this function
will print a message and terminate the script.
NOTE: After a Pyomo model has been constructed, the model can be printed
using the pprint method, model.pprint(). This summarizes the infor-
mation in the Pyomo model, including the constraint and objective expressions.
This can be a very useful debugging tool when a model is not generating the
expected results, since it shows the fully expanded version of the model.
In this example, the Python data for the problem (N, M, d, and P) were explicitly
defined in the script. While this is convenient to create a short book example, in
practice, much more data is often required, and this data would instead be loaded
from another source (e.g., an Excel file, or JSON file).
36 3 Pyomo Overview
Consider Figure 3.1, showing some example data for the warehouse location
problem specified in Microsoft Excel. The following script loads this data from the
Excel spreadsheet using the Python package Pandas, and then executes the same
lines as before to construct and solve the model, and report the solution.
# wl_excel.py: Loading Excel data using Pandas
import pandas
import pyomo.environ as pyo
from wl_concrete import create_warehouse_model
N = list(df.index.map(str))
M = list(df.columns.map(str))
d = {(r, c):df.at[r,c] for r in N for c in M}
P = 2
Fig. 3.1: This figure shows the data for our warehouse location problem as formatted in Microsoft
Excel.
While data can be specified using native Python types, Pyomo also includes model-
ing components Set and Param to define index sets and parameters respectively.
3.3 Pyomo Models 37
A Pyomo Set component is used to declare valid indices for any indexed com-
ponent. For example, in the context of the warehouse location problem, two sets are
shown: N stores the valid warehouse locations and M stores the customer locations.
These sets can easily be declared using the following code:
model.N = pyo.Set()
model.M = pyo.Set()
This example passes Set objects into the Var constructor, rather than the Python
lists used in earlier examples. A Set component can be initialized by employing
the initialize keyword argument, with a Python set, list, or tuple.
Pyomo Set objects can also be indexed by other sets. Consider the following
example:
model.PremierSundaes = pyo.Set()
model.Toppings = pyo.Set(model.PremierSundaes)
This example declares a scalar parameter P and an indexed parameter d. The pa-
rameter d is indexed by the Pyomo sets for the warehouse and customer locations
defined earlier. As with the Set object, values for these parameters could be pro-
vided through the initialize keyword argument using a Python dictionary or
by defining a construction rule.
By default, parameters are immutable, meaning once their values are set, these
values cannot be changed. This default behavior allows for increased efficiency
within Pyomo when handling expressions. However, a parameter whose values are
mutable can be defined with the mutable=True keyword argument. This can be
useful if a model should be solved multiple times with different values of some of
the parameters.
As an example, consider the warehouse location problem again. Assume signif-
icantly more data is required (e.g, a large number of potential warehouse locations
and customer locations). Using a mutable parameter for P easily shows how the op-
timal delivery costs change when the maximum number of warehouses is changed.
38 3 Pyomo Overview
The following code shows the process to define the model with a mutable param-
eter for P using the Pyomo Param object.
# wl_mutable.py: warehouse location problem with mutable param
import pyomo.environ as pyo
def obj_rule(mdl):
return sum(d[n,m]*mdl.x[n,m] for n in N for m in M)
model.obj = pyo.Objective(rule=obj_rule)
def num_warehouses_rule(mdl):
return sum(mdl.y[n] for n in N) <= mdl.P
model.num_warehouses = \
pyo.Constraint(rule=num_warehouses_rule)
return model
The key differences are the declaration of the Param object, model.P, and the use
of model.P in the num warehouses constraint.
3.3 Pyomo Models 39
The script can be modified to load the distance data from Excel, and execute a
loop in Python to solve the optimization problem repeatedly for different values of
the mutable parameter model.P. This script is shown below.
# wl_mutable_excel.py: solve problem with different values for P
import pandas
import pyomo.environ as pyo
from wl_mutable import create_warehouse_model
N = list(df.index.map(str))
M = list(df.columns.map(str))
d = {(r, c):df.at[r,c] for r in N for c in M}
P = 2
Abstract This chapter describes the core classes used to define optimization models
in Pyomo. Most of the discussion focuses on modeling components used to declare
parts of a model. Included is a discussion of the options used when declaring the
components and information about key component attributes and methods.
41
42 4 Pyomo Components
NOTE: Unless otherwise stated, the code snippets and examples used in this
chapter refer to concrete models.
There are behaviors common across most of the Pyomo modeling components listed
in the previous section. Additionally, there are some common paradigms adopted
4.2 Common Component Paradigms 43
The component c specifies a single constraint in this model, and the component
d specifies a collection of constraints indexed over the set A. The Constraint
component can be used to declare both simple constraints and indexed constraints.
In general, components can also be indexed by multiple index sets. For example,
model.y is indexed over both A and B, and it can be referenced by
model.y[i,j] where i is any valid element from model.A and j is any valid
element from model.B (e.g., model.y[2,’Q’]).
• It is also possible to pass in a Python function to provide the value for every
index in the component. We often call these functions rules.
These uses are illustrated here:
model.A = pyo.Set(initialize=[1,2,3])
model.x = pyo.Var(model.A, initialize=3.14)
model.y = pyo.Var(model.A, initialize={1:1.5, 2:4.5, 3:5.5})
def z_init_rule(m, i):
return float(i) + 0.5
model.z = pyo.Var(model.A, initialize=z_init_rule)
4.3 Variables
Pyomo variables are created using the Var class, which can represent a single value
or an indexed collection of values. Variables can have initial values, and the value of
a variable can be retrieved and set by the user or by a solver as part of the solution
process.
Named and un-named arguments are supported, and Table 4.1 provides a list of the
common arguments that can be passed when declaring the Var component
<un-named> reserved for specifying index sets any number of Pyomo Set objects or
Python lists
within or specifies the valid domain or values a Pyomo Set object, Python list, or
domain for a variable rule function
bounds provides lower and upper bounds for a 2-tuple, or a rule function
the variable
initialize provides an initial value for the vari- a scalar value, Python dictionary of
able index-value pairs, or rule function
The domain of a variable (i.e., the set of legal values) is specified with either the
domain or within keyword options to the Var constructor:
model.A = pyo.Set(initialize=[1,2,3])
model.y = pyo.Var(within=model.A)
model.r = pyo.Var(domain=pyo.Reals)
model.w = pyo.Var(within=pyo.Boolean)
In this example, y is only allowed to take on the integer values 1, 2, or 3. The variable
r can have any real value, and w is restricted to be binary (that is 0/1 or True/False).
If the domain is not specified, the default is the Reals virtual set. Other virtual sets
supported by Pyomo are defined in Table 4.2. Note that these virtual sets can also
be used in other contexts (e.g., when constructing Param objects).
The domain or within argument can also accept a function, which is used to
define the domain for individual elements of an indexed variable. For example:
model.A = pyo.Set(initialize=[1,2,3])
def s_domain(model, i):
return pyo.RangeSet(i, i+1, 1) # (start, end, step)
model.s = pyo.Var(model.A, domain=s_domain)
In this example, s is an indexed variable whose individual entities are defined over
consecutive integer intervals.
NOTE: While Pyomo supports a general representation for restricting the do-
main of the variables, not all solvers support this general behavior. You may
need to restrict your definitions to those supported by the selected solver.
46 4 Pyomo Components
Variable bounds can be explicitly specified with the bounds keyword option:
model.A = pyo.Set(initialize=[1,2,3])
model.a = pyo.Var(bounds=(0.0,None))
The bounds option can specify a 2-tuple with lower and upper values. Alterna-
tively, it can specify a function that returns a 2-tuple for each variable index. Note
that None can be used in place of the lower or upper bound to indicate no bound
should be enforced. In the code snippet above, model.a has a lower bound of 0,
and does not have an upper bound, while model.b has different bounds for each
of its indices. For example, model.b[3] has a lower bound of 6.5 and an upper
bound of 7.5.
The initial value of variables can be set with the initialize keyword argu-
ment as in the following example:
model.A = pyo.Set(initialize=[1,2,3])
model.za = pyo.Var(initialize=9.5, within=pyo.NonNegativeReals)
model.zb = pyo.Var(model.A, initialize={1:1.5, 2:4.5, 3:5.5})
model.zc = pyo.Var(model.A, initialize=2.1)
print(pyo.value(model.za)) # 9.5
print(pyo.value(model.zb[3])) # 5.5
print(pyo.value(model.zc[3])) # 2.1
print(pyo.value(model.m[1])) # 3
print(pyo.value(model.m[3])) # 9
4.4 Objectives 47
When generating formatted output, or scripting advanced workflows, there are sev-
eral attributes and methods of Var commonly used. Consider the following decla-
rations:
model.A = pyo.Set(initialize=[1,2,3])
model.za = pyo.Var(initialize=9.5, within=pyo.NonNegativeReals)
model.zb = pyo.Var(model.A, initialize={1:1.5, 2:4.5, 3:5.5})
model.zc = pyo.Var(model.A, initialize=2.1)
The current value of the variable can be obtained with the value() function, and
the attributes lb and ub hold values for the lower and upper bounds on the variable,
respectively. These values may be inferred from the domain of the variable.
print(pyo.value(model.zb[2])) # 4.5
print(model.za.lb) # 0
print(model.za.ub) # None
The setlb and setub methods are used to set the lower and upper bounds on a
variable.
Variable values can be set using the Python assignment operator,
model.za = 8.5
model.zb[2] = 7.5
One can also call the set values method to set all the variable values from a
dictionary.
Var components can be fixed to specific values. If the fixed attribute is True,
then the variable has a fixed value that will not be altered by an optimizer. The fix
method is used to fix elements of a Var, and the unfix method is used to unfix
elements of a Var.
model.zb.fix(3.0)
print(model.zb[1].fixed) # True
print(model.zb[2].fixed) # True
model.zc[2].fix(3.0)
print(model.zc[1].fixed) # False
print(model.zc[2].fixed) # True
4.4 Objectives
Most solvers can be applied to optimization models with a single objective. The
following code creates an Objective object:
model.a = pyo.Objective()
Named and un-named arguments are supported, and Table 4.3 provides a list of the
common arguments that can be passed when declaring the Objective component.
<un-named> reserved for specifying index sets any number of Pyomo Set objects or
Python lists
expr provides the expression that defines any valid Pyomo expression
the objective function
rule provides the rule function that will be a function that returns a Pyomo ex-
called to access the expression that pression or Objective.Skip
defines the objective function
The expr keyword can be used to specify the actual expression for the objective.
One can also use the rule keyword to specify a rule (a Python function) that re-
turns an expression. A rule provides control over how the objective is formed. Both
options are illustrated here:
model.x = pyo.Var([1,2], initialize=1.0)
def m_rule(model):
expr = model.x[1]
expr += 2*model.x[2]
return expr
model.c = pyo.Objective(rule=m_rule)
4.4 Objectives 49
Some solvers can perform multi-objective optimization with two or more objec-
tives. Multiple objectives can be declared individually or they can be indexed and
defined using a rule as shown here:
A = [’Q’, ’R’, ’S’]
model.x = pyo.Var(A, initialize=1.0)
def d_rule(model, i):
return model.x[i]**2
model.d = pyo.Objective(A, rule=d_rule)
The objective function contains a few attributes that may be useful for scripting or
debugging. The expr attribute stores the expression for the objective. The sense
attribute indicates if the objective is to be minimized or maximized. The value
function can be used to compute the value of the objective. These are illustrated in
the following example:
A = [’Q’, ’R’]
model.x = pyo.Var(A, initialize={’Q’:1.5, ’R’:2.5})
model.o = pyo.Objective(expr=model.x[’Q’] + 2*model.x[’R’])
print(model.o.expr) # x[Q] + 2*x[R]
print(model.o.sense) # minimize
print(pyo.value(model.o)) # 6.5
50 4 Pyomo Components
4.5 Constraints
A constraint defines one or more expressions that place limits on the feasible values
of variables. The declaration of constraint expressions is similar to the declaration
of objective function expressions. Constraints differ from objectives in that the ex-
pressions include relationships (equalities or inequalities). While objectives can be
indexed, this feature is infrequently used. In contrast, constraints are commonly in-
dexed, allowing for a collection of related constraint expressions to be constructed
and stored in a single constraint object.
Several named arguments are supported, and Table 4.4 lists the common arguments
that can be passed when declaring a Constraint component.
The expression specified by the expr keyword can alternatively be generated
with a rule function. For example, the diff constraint can also be declared as
follows:
model.x = pyo.Var([1,2], initialize=1.0)
def diff_rule(model):
return model.x[2] - model.x[1] <= 7.5
model.diff = pyo.Constraint(rule=diff_rule)
<un-named> reserved for specifying index sets any number of Pyomo Set objects or
Python lists
expr provides the expression that defines any valid Pyomo expression with a
the constraint relational operator, a 2-tuple, or a 3-
tuple
rule provides the rule function that will be a function that returns a Pyomo
called to access the expression that expression with a relational op-
defines the constraint erator, a 2-tuple, a 3-tuple, or
Constraint.Skip
Constraints can be indexed, and those indices can be used to refer to specific
elements of indexed parameters and variables when constructing expressions. The
following code fragment shows an example of this:
N = [1,2,3]
Indexed constraints are specified in the same manner as indexed objectives. Py-
omo iterates over the cross product of the indexing sets, providing an index from
each set to the rule function. The CoverConstr constraint in this example imple-
ments the following mathematical model:
ai yi ≥ bi ∀i ∈ {1, 2, 3} (4.1)
Given the data specified in a and b, the model instance passed to the solver will
include the following explicit constraints:
y[1] ≥ 1
3.1 · y[2] ≥ 2.9
4.5 · y[3] ≥ 3.1
where expr1 and expr2 may be non-constant expressions. (Note that < and >
are not supported.)
• equality constraints have the form
expr1 = expr2
where lower and upper are constant expressions and expr1 is a non-constant
expression.
52 4 Pyomo Components
In some optimization models, a constraint might not be defined for all indices.
For example, particular indices might not be physically realizable. The rule function
can return Constraint.Skip (or Constraint.NoConstraint) to indicate
that no constraint is associated with a particular index. For example, consider the
declaration of a notional task scheduling constraint:
TimePeriods = [1,2,3,4,5]
LastTimePeriod = 5
The value of the constraint body can be evaluated using the value function.
Similarly, the lslack and uslack methods can be used to compute slack values
(the difference between the current expression value and the lower or upper bound),
as shown in the following example:
model = pyo.ConcreteModel()
model.x = pyo.Var(initialize=1.0)
model.y = pyo.Var(initialize=1.0)
print(pyo.value(model.c1.body)) # 0.0
print(model.c1.lslack()) # inf
print(model.c1.uslack()) # 7.5
print(model.c2.lslack()) # 2.5
print(model.c2.uslack()) # inf
print(model.c3.lslack()) # 3.0
print(model.c3.uslack()) # 7.0
A set is a collection of data, possibly including numeric data (e.g., real or integer
values) as well as symbolic data (e.g., strings) typically used to specify the valid
indices for an indexed component. Several classes can be used to define sets in
Pyomo models:
Set A generic component for declaring sets
RangeSet A component that defines a range of numbers
SetOf A component that creates a set from external data without
copying the data
Named and un-named arguments are supported, and Table 4.5 provides a list of the
common arguments that can be passed when declaring the Set component.
54 4 Pyomo Components
<un-named> reserved for specifying index sets any number of Pyomo Set objects or
Python lists
initialize provides initial values to store in the Python list, Python dictionary, or rule
set function
within specifies the valid values that can be a Pyomo Set object or Python list
domain contained in the set
bounds specifies the lower and upper bounds a 2-tuple, a Python dictionary, or a
for valid values in the set rule function
An indexed set can also be specified by providing other sets or Python lists as
un-named arguments in the declaration:
model.A = pyo.Set()
model.B = pyo.Set()
model.C = pyo.Set(model.A)
model.D = pyo.Set(model.A,model.B)
Set declarations can also use standard set operations to declare a set in a constructive
fashion:
model.A = pyo.Set()
model.B = pyo.Set()
model.G = model.A | model.B # set union
model.H = model.B & model.A # set intersection
model.I = model.A - model.B # set difference
model.J = model.A ˆ model.B # set exclusive-or
The previous examples illustrate how data can be specified or dynamically gen-
erated to initialize a set. There are some contexts where it is simpler to specify the
set elements that should be omitted. The filter keyword can be used to specify a
function that returns True when an element belongs in a set, and False otherwise.
For example:
model.P = pyo.Set(initialize=[1,2,3,5,7])
def filter_rule(model, x):
return x not in model.P
model.Q = pyo.Set(initialize=range(1,10), filter=filter_rule)
Here, set P contains prime values, and set Q is the set of all numbers except for the
members of P.
After an indexed set is constructed in a concrete model, sets can be added for
specific indices using the Python equal operator:
model.R = pyo.Set([1,2,3])
model.R[1] = [1]
model.R[2] = [1,2]
Validation of set data is supported in two different ways. First, a superset can be
specified with the within or domain keyword:
model.B = pyo.Set(within=model.A)
When an element is added to the set B, it is checked to confirm that it also belongs
to A. This ensures B is a subset of A.
Validation of set data can also be performed by passing a rule to the validate
keyword argument. The rule function should return True if the element that is
passed in belongs in this set, and False otherwise (Pyomo will throw an excep-
tion). For example, the following C validate function mimics the within key-
word argument:
def C_validate(model, value):
56 4 Pyomo Components
Finally, note that if both the within and validate keyword arguments are spec-
ified, then the logic specified by both are applied to validate set elements.
By default, sets are ordered by insertion order. In some cases, we may want the
set elements to be in sorted order. This can be done using the Set.SortedOrder
option with the ordered keyword:
model.A = pyo.Set(ordered=pyo.Set.SortedOrder)
Sets may contain data elements that are either singletons or k-tuples. The dimen
keyword is used to specify the expected dimension of the data. The default value
is one, indicating the set will contain singleton data. In some cases, the appropriate
value of the dimension can be determined from other keyword values, but in general
the user is required to specify this keyword for tuple set data.
Ordered sets may have first and last values. The bounds option can be used to
specify a 2-tuple defining upper and lower bounds for a set. This option may be
inferred from the within argument, when the set is ordered.
The RangeSet component defines an ordered virtual set that represents a se-
quence of integer or floating point values. This sequence is defined by a start value,
a final value, and a step size. If a RangeSet is defined with a single argument, then
the argument defines the final value. The start value defaults to 1 and the step size
defaults to 1. For example, the following defines a sequence of integers from 1 to
10:
model.A = pyo.RangeSet(10)
If a RangeSet is defined with two arguments, then the first is the start value and the
second is the final value. For example, the following defines a sequence of integers
from 5 to 10:
model.C = pyo.RangeSet(5,10)
Finally, if a RangeSet is defined with three arguments, then they are the start
value, final value and step size respectively. For example, the following defines a
sequence of floating point values from 2.5 to 10.0 with step 1.5:
model.D = pyo.RangeSet(2.5,11,1.5)
print(len(model.A)) # 3
4.6 Set Data 57
The elements in the set can be accessed with the data() method, which returns
the underlying set data as a Python tuple (or a Python dictionary for indexed sets)
as shown below:
model.A = pyo.Set(initialize=[1, 2, 3])
model.B = pyo.Set(initialize=[3, 2, 1], ordered=True)
model.C = pyo.Set(model.A, initialize={1:[1], 2:[1, 2]})
Set comparison and membership tests are supported with a variety of Python
operators:
model.A = pyo.Set(initialize=[1,2,3])
Sets can also be iterated over to access individual elements in the set:
model.A = pyo.Set(initialize=[1, 2, 3])
model.C = pyo.Set(model.A, initialize={1:[1], 2:[1, 2]})
Ordered sets include a variety of methods that reflect the ordering in the set:
model.A = pyo.Set(initialize=[3, 2, 1], ordered=True)
print(model.A.first()) # 3
print(model.A.last()) # 1
print(model.A.next(2)) # 1
print(model.A.prev(2)) # 3
print(model.A.nextw(1)) # 3
print(model.A.prevw(3)) # 1
The first() and last() methods return the first and last elements in an or-
dered set respectively. The next() method takes an element in the set and returns
the next element in the set. Similarly, the prev() method returns the previous el-
ement. The nextw() and prevw() methods operate similarly, except that they
wrap around the ends of the set. In this example, the value of nextw(1) is 3 be-
cause 1 is the last element of the set, and 3 is the next element if the set indices wrap
around. The ord() method can be used to find the position index of an element in
an ordered set, and the [] operator can be used to access an element given a position
index:
model.A = pyo.Set(initialize=[3, 2, 1], ordered=True)
print(model.A.ord(3)) # 1
print(model.A.ord(1)) # 3
print(model.A[1]) # 3
print(model.A[3]) # 1
NOTE: The position indices start at one, not zero. The order of the set is de-
termined by the sequence of the data provided when it is instantiated and the
option specified for the ordered keyword argument.
An unindexed Param component looks a lot like a scalar value, and an indexed
Param component looks a lot like a Python dictionary of values. The Param com-
ponent supports advanced features like mutability and sparse representations with
default values.
Named and un-named arguments are supported, and Table 4.6 provides a list of the
common arguments that can be passed when declaring the Param component.
<un-named> reserved for specifying index sets any number of Pyomo Set objects or
Python lists
initialize provides an initial value for the pa- a scalar value, Python dictionary, or
rameter rule function
default provides default a value to use for the a scalar value, Python dictionary, or
parameter if no value has been set rule function
validate specifies a function that is called to a function that returns True or False
determine if a particular value is valid given a particular value
for the parameter
If ordered sets are used to define the index for an indexed parameter, then the ini-
tialization function can reference previously defined parameter values:
def XX_init(model, i, j):
if i==1 or j==1:
return i*j
return i*j + model.XX[i-1,j-1]
model.XX = pyo.Param(model.A, model.A, initialize=XX_init)
The default option can be used to specify parameter values for all valid in-
dices that have not been explicitly initialized. For example, we can define an indexed
parameter that represents a 3 × 3 diagonal matrix as follows:
u={}
u[1,1] = 10
u[2,2] = 20
u[3,3] = 30
model.U = pyo.Param(model.A, model.A, initialize=u, default=0)
Similar to the Set component, there are two ways to validate parameter val-
ues. First, the within keyword option can be used to specify the valid domain of
parameter values:
model.Z = pyo.Param(within=pyo.Reals)
Validation of parameter data can also be performed with the validate option,
which specifies a function that returns True if a parameter value is valid and
False if it is not (Pyomo will throw an exception). The following example uses
the validate option to mimic the behavior of the within option:
def Y_validate(model, value):
return value in pyo.Reals
model.Y = pyo.Param(validate=Y_validate)
If both the within and validate options are specified, then the logic for both
of these options will be applied to validate parameter values.
The Param component can be used to represent constant values in Pyomo mod-
els; however, mutability is also supported. In the following example, Pyomo gener-
4.7 Parameter Data 61
ates the expression for the objective in this model with the form:
x1 + 4x2 + 9x3 .
Specifically, Pyomo has treated parameter values as fixed constants, and its expres-
sions simply contain the numeric constants.
model = pyo.ConcreteModel()
p = {1:1, 2:4, 3:9}
model.A = pyo.Set(initialize=[1,2,3])
model.p = pyo.Param(model.A, initialize=p)
model.x = pyo.Var(model.A, within=pyo.NonNegativeReals)
Note that this “conversion” happens as soon as the expression is first created. The
fact that these values come from a Param component is lost, and only the numerical
values remain. This is done for efficiency. Consequently, these values cannot be
changed once the expression is created.
However, this behavior is different if the mutable option is specified while
constructing the model. If this option is True, then the parameter values are not
treated as constants. Consider the previous example again where the p parameter is
now mutable:
model = pyo.ConcreteModel()
p = {1:1, 2:4, 3:9}
model.A = pyo.Set(initialize=[1,2,3])
model.p = pyo.Param(model.A, initialize=p, mutable=True)
model.x = pyo.Var(model.A, within=pyo.NonNegativeReals)
model.p[2] = 4.2
model.p[3] = 3.14
When Pyomo generates the expression for the objective in this model, it keeps
knowledge of the Param component and now has the form:
p1 x1 + p2 x2 + p3 x3 ,
where the values pi are Param objects with references to the parameter values.
Here, Pyomo treats the parameter values as mutable values that may be changed later
by the user. In this example, the parameter values are changed after the objective
expression is defined, and the resulting objective is
x1 + 4.2x2 + 3.14x3 .
The parameters are only replaced with their numerical values when calling the
solver. Therefore, their values can be changed between consecutive calls to a solver.
62 4 Pyomo Components
Mutable parameters require some additional overhead for memory and they re-
quire additional processing when translating Pyomo expressions into a form that a
solver understands. Consequently, parameters are immutable by default.
Pyomo assumes parameter values are specified with a sparse representation. For
example, the Param object T declares a parameter indexed over sets A and B:
model.T = pyo.Param(model.A, model.B)
However, not all of these values are required to be defined in a model. For example:
model.B = pyo.Set(initialize=[1,2,3])
w={}
w[1] = 10
w[3] = 30
model.W = pyo.Param(model.B, initialize=w)
Parameter W is defined for indices 1 and 3, but the index set B includes 1, 2, and 3.
If W[2] is accessed, an error occurs and a Python exception is thrown.
As mentioned earlier, a default value can also be provided with the default
keyword argument. If a default value is provided, and a model tries to access a
value that has not been initialized, the default value is used (instead of throwing an
exception). Note that the parameter data is stored with a sparse representation, even
if the default value is specified. This is supported for memory efficiency. It provides
a convenient way for the modeler to reference sparse values without adopting a
specialized data structure.
Because of this sparse representation, several methods that consider the valid
keys of an indexed parameter require specialized behavior. Let the valid index set
refer to the complete list of all valid indices whether initialized or not, and let the
effective index set denote only the set of initialized key values in an indexed compo-
nent. If no default value is declared, then the the len function returns the size of the
effective index set, and the in operator tests if a specified value is in the effective
index set. Iteration is supported over values in the effective index set, and the Python
[] operator can be used to access individual elements (which is the parameter value
in this example).
If a default value is declared, then all indices are equally valid in the model,
whether explicitly indexed or not. Therefore, the len() function returns the size
of the full index set, iteration and the in operator consider the full index set. Thus,
when a default value is specified, the parameter appears to be densely populated
with values, even if the underlying data structure is kept sparse for efficiency. This
is illustrated in the following example:
model = pyo.ConcreteModel()
model.p = pyo.Param([1,2,3], initialize={1:1.42, 3:3.14})
model.q = pyo.Param([1,2,3], initialize={1:1.42, 3:3.14}, \
default=0)
4.8 Named Expressions 63
Named and un-named arguments are supported, and Table 4.7 provides a list of the
common arguments that can be passed when declaring the Expression compo-
nent.
64 4 Pyomo Components
The expr or rule keywords can be used to initialize a named expression when
it is declared, as shown in the following example:
model.x = pyo.Var()
model.e1 = pyo.Expression(expr=model.x + 1)
def e2_rule(model):
return model.x + 2
model.e2 = pyo.Expression(rule=e2_rule)
<un-named> reserved for specifying index sets any number of Pyomo Set objects or
Python lists
rule provides the rule function that will a function that returns a Pyomo ex-
be called to provide the expression to pression or Expression.Skip
store
A simple use for the Expression component declares a single expression and
uses it inside an objective and a constraint declaration:
model.x = pyo.Var()
model.e = pyo.Expression(expr=(model.x - 1.0)**2)
model.o = pyo.Objective(expr=0.1*model.e + model.x)
model.c = pyo.Constraint(expr=model.e <= 1.0)
4.9 Suffix Components 65
The value of the named expression can be computed using the value function.
Additionally, the expression stored in the named Expression component can be up-
dated. As the following example shows, updating the named expression has the
effect of updating the objective and constraint expressions where it is used:
model.x.set_value(2.0)
print(pyo.value(model.e)) # 1.0
print(pyo.value(model.o)) # 2.1
print(pyo.value(model.c.body)) # 1.0
model.e.set_value((model.x - 2.0)**2)
print(pyo.value(model.e)) # 0.0
print(pyo.value(model.o)) # 2.0
print(pyo.value(model.c.body)) # 0.0
Suffixes provide a mechanism for annotating a model with auxiliary data not strictly
related to the model declaration and structure. Suffixes are commonly used by solver
plugins to store extra information about the solution of a model. More generally,
suffixes can be used to
• import information from a solver about the solution to a mathematical program
(e.g., constraint duals, variable reduced costs, basis information),
• export information to a solver or algorithm to configure the solution process
(e.g., warm-starting information, variable branching priorities), and
• tag model components with local data for later use in advanced scripting algo-
rithms.
This functionality is made available to the modeler through the Suffix compo-
nent class, providing an interface for annotating Pyomo modeling components with
additional data.
Named arguments are supported, and Table 4.8 provides a list of the common argu-
ments that can be passed when declaring the Suffix component
Suffixes are not guaranteed to be compatible with all solver plugins in Pyomo.
Whether a given suffix is acceptable or not depends on both the solver and solver
interface being used. In some cases, a solver plugin will raise an exception if it en-
counters a suffix type that it does not handle, but this is not true in every situation.
For example, the nl file interface is generic to all AMPL-compatible solvers, so
there is no way for Pyomo to validate that a suffix of a given name, direction, and
datatype is appropriate for a solver. One should be careful in verifying that suffix
declarations are being handled as expected when switching to a different solver or
solver interface.
The initialize keyword argument can be used to define suffix values. This
argument specifies a function that is executed when the model is constructed. This
function returns a list or iterable of (component, value) tuples.
model = pyo.AbstractModel()
model.x = pyo.Var()
model.c = pyo.Constraint(expr=model.x >= 1)
def foo_rule(m):
return ((m.x, 2.0), (m.c, 3.0))
model.foo = pyo.Suffix(initialize=foo_rule)
This examples includes two variable components, indexed and non-indexed, along
with a suffix component. Conceptually, the declaration of the suffix foo allows the
association of foo with each component in the model. For example:
# Assign the value 1.0 to suffix ’foo’ for model.x
model.x.set_suffix_value(’foo’, 1.0)
Suffix values can be assigned with set suffix value and they can be accessed
with get suffix value. This example illustrates two ways of specifying the
same suffix: with a name and with a suffix component object.
68 4 Pyomo Components
This example illustrates how set suffix value is used to set the value for an
indexed component and a single component data object. When
set suffix value is called for an indexed component, by default it sets suf-
fix values for all elements or indices of the component, rather than the component
itself. Because of this, when we try to retrieve the suffix value for the model.y
component, we find that it is None.
Suffix values can also be cleared, which is equivalent to setting the value None:
model.y[3].clear_suffix_value(model.foo)
print(model.y.get_suffix_value(model.foo)) # None
print(model.y[1].get_suffix_value(model.foo)) # 3.0
print(model.y[2].get_suffix_value(model.foo)) # 4.0
print(model.y[3].get_suffix_value(model.foo)) # None
This chapter presented details for some of the most common modeling components
supported by Pyomo. There are other modeling components that were not thor-
oughly discussed in this chapter. These include:
Block: The Block component provides a mechanism to declare models with re-
peated or nested structure (e.g., separate Block objects may exist on a model to
represent different time points in a multi-period optimization). A Block con-
sists of a collection of Pyomo modeling components. More discussion of blocks
is provided in Chapter 8.
Model: The Model component provides a container for grouping Pyomo modeling
components to form the definition of an optimization problem. Pyomo supports
both abstract and concrete modeling representations. While “model” objects
were widely used in this book (they are required to formulate and solve an
optimization problem in Pyomo), we have not discussed the fact that they are
components themselves. In fact, they inherit from the Block component.
4.10 Other Modeling Components 69
Abstract This chapter illustrates the use of Python with Pyomo for solution anal-
ysis and the development of custom workflows or high-level meta-algorithms. For
example, the chapter shows how to access variable and objective values, add and
remove constraints, and iterate over model components. This chapter also contains
some larger examples, to illustrate how Pyomo users can go beyond the basics and
develop custom solution and analysis strategies.
5.1 Introduction
71
72 5 Scripting Custom Workflows
In this chapter, the basics of scripting with Pyomo will be discussed. This func-
tionality will be demonstrated on some examples, including a Sudoku solver.
NOTE: This chapter shows the power of Pyomo that can be accessed through
the Python language, and examples in this chapter may make use of methods
on components that are part of the core Pyomo infrastructure. The developers
of Pyomo try to maintain backwards compatibility where possible. However,
note that the methods described in this chapter are more likely to change than
other capabilities discussed in this book.
This script requires two additional files. The standard Python distribution in-
cludes support for reading and writing JSON files.
5.1 Introduction 73
def obj_rule(m):
return sum(m.dist[w][c]*m.x[w,c] for w in m.WH for c in \
m.CUST)
model.obj = pyo.Objective(rule=obj_rule)
def num_warehouses_rule(m):
return sum(m.y[w] for w in m.WH) <= m.P
model.num_warehouses = \
pyo.Constraint(rule=num_warehouses_rule)
return model
Python allows scripting of custom workflows much more powerful than this sim-
ple example. In this chapter, a description of the steps to access model components
and modify the model programmatically will be illustrated
Instances of Pyomo modeling objects (e.g., Var, Param, Constraint) have sev-
eral attributes that can be accessed programmatically. These attributes can be inter-
5.2 Interrogating the Model 75
rogated to provide information about the state of the model. In this section, we show
how to iterate over model components and access key attributes of these compo-
nents. Chapter 4 provides additional details about the attributes of Pyomo modeling
components.
For example, one can access the actual expression for the objective function.
Consider the warehouse location problem described previously. The following code
will print the expression for the objective function, the value of the objective at the
solution, and the value of one of the variables.
import json
import pyomo.environ as pyo
from warehouse_model import create_wl_model
Some Pyomo component attributes may contain Pyomo objects instead of native
Python numerical values. For example, the lower bound of a Var may be a simple
float (e.g., 3.2), but it may also be a Pyomo Param object with an associated
value. Because of this, it is important to use the value function to evaluate at-
tributes that are expected to return numerical values.
76 5 Scripting Custom Workflows
model = pyo.ConcreteModel()
model.u = pyo.Var(initialize=2.0)
In this example, a contains a Pyomo expression object (and not a numerical value
as might have been expected). This is demonstrated when we print it and check the
type. The print statement is correctly showing a description of the expression. To ob-
tain the numeric value of the variable (or other Pyomo components), pyo.value
needs to be called. Here, b contains the numeric value as expected.
This example illustrates the creation of implicit expressions and how they can
lead to unintended consequences. Users are strongly encouraged to avoid generating
expressions except as part of the model construction process.
In this case, the code will print the variable name “y”, not the value.
The previous examples illustrate the procedure to access values of scalar variables.
The value of a particular index of an indexed variable by can be retrieved by speci-
fying the exact index.
print(pyo.value(model.y[’Ashland’]))
It is also possible to access all the values by iterating over each element of an indexed
variable.
for i in model.y:
print(’{0} = {1}’.format(model.y[i], pyo.value(model.y[i])))
5.2 Interrogating the Model 77
The loop above iterates over the keys for the indexed variable model.y. This ex-
ample could also have been written to iterate over the index set directly as shown
below.
for i in model.WH:
print(’{0} = {1}’.format(model.y[i], pyo.value(model.y[i])))
This approach can also be used to access different attributes of variables (e.g.,
lower bound model.y[i].lb) and other model components.
Pyomo supports advanced slicing notation to allow for more control when looping
over individual elements of a model component. For example, we can see all of the
customers that are served from a particular warehouse with the following slicing
notation:
for v in model.x[’Ashland’,:]:
print(’{0} = {1}’.format(v, pyo.value(v)))
NOTE: Note that the slicing notation is returning the component objects them-
selves, and not their indices. Additional information can be found in Sec-
tion 8.5.
Pyomo also provides methods to generically loop over components on a given model
or block. In this short example, we show how to iterate over all Var objects on a
model.
# loop over the Var objects on the model
for v in model.component_objects(ctype=pyo.Var):
for index in v:
print(’{0} <= {1}’.format(v[index], \
pyo.value(v[index].ub)))
NOTE: These methods can be used to find all components on a Pyomo model
and iterate over them. This approach also extends to hierarchical models with
Block components. For a deeper dive into these methods, consult the online
documentation.
78 5 Scripting Custom Workflows
A frequent use case is the need to repeatedly solve models with different parameter
values or minimal changes to the constraints. Mutable parameters can be used to
efficiently solve models with different parameter values. An example showing the
use of the mutable Param P is shown in Section 3.3.5.
Within Pyomo, it is possible to modify the structure of a model between solves.
For example:
• Objectives and constraints can be activated and deactivated without changing
the data stored in the model. A deactivated component will be excluded when
the Pyomo model is being sent to the solver.
• Variables can be treated as fixed or unfixed (the default).
• Pyomo also allows addition and removal of modeling components. For example,
constraints can be added or removed from a model.
The following example illustrates the use of these approaches for modifying
model structure:
import pyomo.environ as pyo
model = pyo.ConcreteModel()
model.x = pyo.Var(bounds=(0,5))
model.y = pyo.Var(bounds=(0,1))
model.con = pyo.Constraint(expr=model.x + model.y == 1.0)
model.obj = pyo.Objective(expr=model.y-model.x)
# add a constraint
model.con2 = pyo.Constraint(expr=4.0*model.x + model.y == 2.0)
solver.solve(model)
print(pyo.value(model.x)) # 0.33
print(pyo.value(model.y)) # 0.66
# deactivate a constraint
model.con.deactivate()
solver.solve(model)
print(pyo.value(model.x)) # 0.5
print(pyo.value(model.y)) # 0.0
# re-activate a constraint
model.con.activate()
solver.solve(model)
print(pyo.value(model.x)) # 0.33
print(pyo.value(model.y)) # 0.66
# delete a constraint
5.4 Examples of Common Scripting Tasks 79
del model.con2
solver.solve(model)
print(pyo.value(model.x)) # 1.0
print(pyo.value(model.y)) # 0.0
# fix a variable
model.x.fix(0.5)
solver.solve(model)
print(pyo.value(model.x)) # 0.5
print(pyo.value(model.y)) # 0.5
# unfix a variable
model.x.unfix()
solver.solve(model)
print(pyo.value(model.x)) # 1.0
print(pyo.value(model.y)) # 0.0
In this section, we will show a few scripting examples that illustrate the capabilities
shown previously. More examples can be found in the Pyomo Gallery (see www.
pyomo.org).
The following example formulates the warehouse location problem and solves it
repeatedly to find every possible solution. Each time a solution is found, a new cut
is added that excludes that solution, and the problem is solved again to find the
next solution. This process is repeated until the problem is infeasible, and no more
solutions can be found.
80 5 Scripting Custom Workflows
The ConstraintList component is used to contain the list of cuts; each time
through the loop a new cut is added to this component.
import json
import pyomo.environ as pyo
from warehouse_model import create_wl_model
import matplotlib.pyplot as plt
term_cond = results.solver.termination_condition
print(’’)
print(’--- Solver Status: {0} ---’.format(term_cond))
if pyo.check_optimal_termination(results):
# look at the solution
print(’Optimal Obj. Value = \
{0}’.format(pyo.value(model.obj)))
objective_values.append(pyo.value(model.obj))
model.y.pprint()
x = range(1, len(objective_values)+1)
plt.bar(x, objective_values, align=’center’)
plt.gca().set_xticks(x)
plt.xlabel(’Solution Number’)
plt.ylabel(’Optimal Obj. Value’)
plt.savefig(’WarehouseCuts.pdf’)
5.4 Examples of Common Scripting Tasks 81
This example generates console output that shows each of the solutions encoun-
tered. It also generates Figure 5.1 with the package matplotlib that shows the
value of the optimal objective function for each solution obtained.
5000
4000
Optimal Obj. Value
3000
2000
1000
0
1 2 3 4 5 6
Solution Number
Fig. 5.1: Optimal objective value for a series of solutions obtained from the warehouse location
problem.
In this section, we further illustrate the power of scripting in Python with Pyomo.
Specifically, we will solve a feasibility problem and show how to find all the feasible
solutions to the Sudoku puzzle. We will solve the problem once, identify a feasible
solution, then add an integer cut to remove this solution from the list of possible
solutions, and solve the problem again.
A typical Sudoku puzzle is shown in Figure 5.2. In this puzzle, one must fill in the
empty cells with the numbers 1 through 9. Each row must have only one occurrence
of each number. Likewise, each column must only have one occurrence of each
number. Finally, each of the nine sub-squares must also only have one occurrence of
each number. We define the sets ROW S, COLS, and VALUES (all of which contain
the integers 1 through 9. We then define a binary variable y[r, c, v] to indicate which
number is in each of the cells. If y[r, c, v] = 1, then this implies that the value v has
been selected for the cell identified by row r and column c.
82 5 Scripting Custom Workflows
! " #
$ % & !
& ' $
' $ "
( ' " %
# ) $
$ ) '
( % & !
' # &
Defining the constraint that restricts the number for the sub-squares is a little
more difficult. To make the definition easier, we define a set with an index for each
of the sub-squares. Then, we define a list of tuples that describes the map from
each of the sub-squares to the list of corresponding indices. This list, along with
the corresponding sub-squares constraint, is defined in the complete code listing for
this example at the end of this section. The desired constraint for the sub-squares is
given by,
5.4 Examples of Common Scripting Tasks 83
∑ y[r, c, v] = 1 ∀ i ∈ SUBSQUARES.
(r,c)∈ssmap [i]
The last key constraint for the Sudoku problem is to make sure that there is only
one value allowed per cell. The constraint is given by,
When designing Sudoku puzzles, two features may change frequently: the initial
board layout and the number of integer cuts to remove previously seen solutions.
One way to handle this variety of potential inputs is to define a function to create
the model from a starting puzzle as well as a list of integer cuts. However, such a
function would be inefficient for our purposes since we would be creating an entirely
new model each time we wanted to add a single new integer cut after each solve.
Thus, we will define two separate functions: one that creates the initial model given
a Sudoku board, and another that adds a new integer cut to the given model based
on the current value of its variables.
We define an integer cut using two sets. The first set S0 consists of indices for
those variables whose current solution is 0, and the second set S1 consists of indices
for those variables whose current solution is 1. Given these two sets, an integer cut
constraint that would prevent such a solution from appearing again is defined by,
model = pyo.ConcreteModel()
return model
5.4 Examples of Common Scripting Tasks 85
The following code shows a script that drives the optimization process based on
these three functions. This script defines the candidate board, and iteratively solves
the Sudoku problems by adding integer cuts until the problem is no longer feasible.
Infeasibility is assumed when the solver termination condition is no longer reported
as optimal.
from pyomo.opt import (SolverFactory,
TerminationCondition)
from sudoku import (create_sudoku_model,
print_solution,
add_integer_cut)
model = create_sudoku_model(board)
solution_count = 0
while 1:
if results.solver.termination_condition != \
TerminationCondition.optimal:
print("All board solutions have been found")
break
solution_count += 1
add_integer_cut(model)
Running this script provides all possible solutions as the output. In this example,
there is only one solution to the candidate Sudoku puzzle, as shown in Figure 5.3.
Abstract This chapter describes how to interface with solvers in Pyomo. Basic func-
tionality supported by all interfaces includes translation of a Pyomo instance into
the format required by a solver, solver options processing, solver invocation, solve
status checking, and solution loading.
6.1 Introduction
Figure 6.1 shows a high-level view of the relationship between Pyomo and opti-
mization software that we will refer to as a solver. Pyomo is a modeling tool and
does not, itself, include solvers. Rather Pyomo has several interfaces to solvers. As
shown in the figure, Pyomo translates the model into a format used by a solver. The
results of the solver are used to populate the Var objects in the Pyomo model and
a Results object is also returned that can be queried to obtain more information
about solver execution. Additional information can be sent to the solver (e.g., op-
tions) and received from the solver (e.g. dual values) but these are not shown in the
figure.
Pyomo models can be analyzed with a wide variety of optimization solvers, and
there are several types of solver interfaces in Pyomo:
• A shell solver is launched as a separate sub-process by running an executable
found on the user’s PATH environment. Pyomo interfaces with these solvers
through files. Pyomo generates a file description of the problem, launches the
solver, and then loads the results from log files and standard output files. This
is a common form of solver.
• A direct solver is executed as a subroutine. Pyomo interfaces with these solvers
through libraries installed and exposed in the form of Python packages. This is a
less common form of solver, since it relies on the existence of Python interfaces
to solver libraries.
• A persistent solver is related to a direct solver, but includes additional capabil-
ities to allow for incremental modification and re-solve of a model. Persistent
87
88 6 Interacting with Solvers
Fig. 6.1: Stylized representation of typical interactions between Pyomo, which processes the model
and a solver, which computes solutions.
Once a solver object has been constructed, the solver can be invoked by call-
ing the solve() method. The solve() method accepts a number of keyword
arguments, a few of which are shown here, more or less in order of importance.
• options: A dictionary of options to be passed to the underlying solver.
• tee: If this argument is True, then the solver output is printed both to the
standard output as well as saved to the log file. If False (the default), then the
solver output is only saved to log file if the solver creates one.
• load solutions: If this argument is True (the default), then solution
values are automically transfered to Var objects on the model. If False,
6.2 Using Solvers 89
then the results object keeps a raw representation of the solutions and it is
not transferred to the model. It can be transfered to the model using the
model.solutions.load from() method.
• logfile: The filename used to store output for shell solvers.
• solnfile: The filename used to store the solution for shell solvers.
• timelimit: The number of seconds that a shell solver is run before it is ter-
minated. (default is None)
• report timing: If this argument is True, then timing information is re-
ported by the solver (default is False)
• solver io: Used to specify an alternative interface for a solver, e.g. solver io=‘nl‘.
• suffixes: A list of suffixes that are exported to the solver.
The options attribute can be used to send solver specific options to the un-
derlying solver. In the following example, we pass the tee=True keyword argu-
ment to tell Pyomo to print the solver’s execution trace to the terminal. We pass
two solver-specific options to GLPK (sending log output to warehouse.log and
turning off scaling). Notice that some solver-specific options do not take values (e.g.
noscale), but are simply flags to turn on or off particular behavior. For these types
of options, set the option value in the dictionary to None. Note that options can also
be sent directly to the solve function as a dictionary. Options that are passed to
the solve function do not persist.
import json
import pyomo.environ as pyo
from warehouse_model import create_wl_model
After a model is solved, there are two aspects of the solution to be investigated. The
first is the solution status returned from the solver, and the second is the value of the
variables and objective function. The solution status can usually be viewed in the
console by passing tee=True into the solve command, but there are many times
when one would like to read this status in code. This section will discuss the results
object, and show how to retrieve values from variables after the solve.
The solve() method returns a results object that contains status information from
the solver. If the solve completes successfully, the solution values are loaded directly
into the model. This consists of three steps: (1) storing solutions in the solutions
attribute of the model, (2) load the values of variables from a selected solution,
and (3) remove solutions from the results object. Afterwards, the results object
only contains meta-data about the model and the optimization process. For memory
efficiency, it no longer contains the solution itself by default.
Typically, a solver only returns a single solution; however, there are cases where
a solver might return multiple solutions (a pool of solutions). Due to this, the results
object supports an interface that looks like a dictionary of lists containing more than
one solution. However, for the most common case of a single solution, the results
object supports a simple attribute-like interface. The results object returned from
the solve() method contains a problem attribute and a solver attribute that
contain information about the problem statistics and the solver status.
The results.solver attribute contains a SolverInformation object.
Key attributes of this object are shown in the Table 6.1.
As noted in Section 2.5, the simplest thing to do with the results object is
to pass it to the function assert optimal termination , which halts the
script and outputs a message if the solver does not report that it found an opti-
mal solution. In situations where the script should not halt if the solution is opti-
mal, but a test for optimality is needed, the results object can be passed to the
check optimal termination function that returns True if the solver report
optimality and False if not.
In some scripts, it makes sense to defer moving the solution from the results
object to the model until after optimality has been checked. This might be needed
for efficieny or to avoid having Var values that are not optimal loaded into the
6.3 Investigating the Solution 91
termination message String message returned by the solver summarizing the termina-
tion status.
model. To avoid automatic loading of the solution from the results object to the
model, use the load solutions=False argument to the call to solve().
To move the solution values from the results object to the Var values in
the model, use model.solutions.load from(results), which uses the
solutions object that is automatically attached to the model object when it is
passed to the solve method. A short example of these steps is shown:
from pyomo.opt import SolverStatus, TerminationCondition
# Wait to load the solution into the model until
# after the solver status is checked
results = solver.solve(model, load_solutions=False)
if (results.solver.status == SolverStatus.ok) and \
(results.solver.termination_condition == \
TerminationCondition.optimal):
# Manually load the solution into the model
model.solutions.load_from(results)
else:
print("Solve failed.")
Part II
Advanced Topics
Chapter 7
Nonlinear Programming with Pyomo
7.1 Introduction
95
96 7 Nonlinear Programming with Pyomo
min f (x)
x
s.t. c(x) = 0
d L ≤ d(x) ≤ dU
xL ≤ x ≤ xU .
The allowable form of the objective function f (x), the vector of equality constraints
c(x), and the vector of inequality constraints d(x) depends entirely on the solver
selected to provide a solution. However, Pyomo is tested extensively with local and
global solvers that typically assume that these functions are continuous and smooth,
with continuous first (and possibly second) derivatives. The development of nonlin-
ear extensions for Pyomo has focused on this broad problem class.
Table 7.1: Python operators that have been redefined to generate Pyomo expressions.
Table 7.2: Functions supported by Pyomo for the definition of nonlinear expressions. This table
assumes that Pyomo has been imported with import pyomo.environ as pyo.
In this section we present a short example to illustrate the formulation and solution
of a nonlinear Pyomo model. We consider the unconstrained minimization of the
two-variable Rosenbrock function, which is a classic problem frequently used as
an example for discussion of unconstrained nonlinear optimization algorithms (e.g.,
see [45]). This problem is defined as
2
min f (x, y) = (1 − x)2 + 100 y − x2 ,
x,y
98 7 Nonlinear Programming with Pyomo
and the solution is in the bottom of the banana shaped valley at the point x=1 and
y=1 (See Figure 7.1).
2
Fig. 7.1: Surface plot of the Rosenbrock function f (x, y)= (1 − x)2 + 100 y − x2 . The minimum
is in the bottom of a banana shaped valley at the point x=1, y=1.
model = pyo.ConcreteModel()
model.x = pyo.Var(initialize=1.5)
model.y = pyo.Var(initialize=1.5)
def rosenbrock(model):
return (1.0 - model.x)**2 \
+ 100.0*(model.y - model.x**2)**2
model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize)
status = pyo.SolverFactory(’ipopt’).solve(model)
pyo.assert_optimal_termination(status)
model.pprint()
This example illustrates that defining a nonlinear model is really no different from
defining a linear model. The model creates two variables x and y and initializes each
7.3 Solving Nonlinear Programming Formulations 99
of them to a value of 1.5. Notice that there is no need to provide any indication the
variables will later appear in a nonlinear expression; this will be deduced by Pyomo
before solving the problem. The construction rule for the objective function simply
returns a nonlinear expression. The nonlinear solver I POPT is then used to solve the
problem, followed by a check of the solver status and printing of the solution.
1 Objective Declarations
obj : Size=1, Index=None, Active=True
Key : Active : Sense : Expression
None : True : minimize : (1.0 - x)**2 + 100.0*(y -
x**2)**2
3 Declarations: x y obj
In this output, we see that the problem is correctly solved to a value of x=y=1.0,
with an objective value of essentially zero. While this example has only a single
nonlinear objective and two scalar variables, the modeling components discussed in
earlier chapters may also be used.
In most cases, an appropriate nonlinear solver must be installed before Pyomo can
be used to optimize a nonlinear programming model. Pyomo’s capabilities are fo-
cused on modeling optimization applications, and there are a limited number of
solvers directly integrated with Pyomo.
100 7 Nonlinear Programming with Pyomo
Nonlinear programming solvers require the modeling framework to evaluate the ob-
jective function and constraints at candidate points in x. As well, many nonlinear
solvers also require evaluation of first, and often second, derivatives at candidate
points. However, a Pyomo user does not have to implement these computations.
Instead, automatic differentiation (AD) tools are used to provide accurate and ef-
ficient numerical evaluation of the first and second derivatives without any user
involvement.
Pyomo provides interfaces for nonlinear solvers compiled for either AMPL or
GAMS, and it utilizes the model evaluation and automatic differentiation (AD) ca-
pabilities provided by these tools to support efficient computation of derivatives. By
supporting these interfaces, a wide array of solvers are immediately available for
use with Pyomo without the need to develop individual interfaces for each solver.
In the case of AMPL solvers, the solver code itself is compiled with the AMPL
Solver Library (ASL) interface [23], which is in the public domain. Therefore, there
are a number of open-source solvers available to Pyomo through this interface. The
Pyomo-GAMS interface performs a translation of the Pyomo model into a GAMS
model, and consequently a user will need to install GAMS to use GAMS-specific
solvers.
Variable Initialization
Solvers for nonlinear programming problems often require the initialization of prob-
lem variables. If initial values are not specified, then Pyomo assumes that the initial
values are zero. However, these default values cannot be relied on in many applica-
tions.
For the general nonconvex case, nonlinear programming problems can, and of-
ten do, have multiple local solutions. While significant advances have been made
in global optimization (i.e., rigorous methods that provide a guarantee of global
optimality), general large-scale problems are often still intractable, even with state-
of-the-art global solvers. Consequently, one is often forced to employ a solver that
only provides a guarantee of local optimality. And, it is often critical to initialize the
problem effectively to ensure convergence to a desirable local solution.
7.3 Solving Nonlinear Programming Formulations 101
Sometimes, the undesired local solutions are not physically meaningful, and a
sensible initialization with reasonable variable bounds is sufficient to ensure reli-
able progress to the desired solution. Other times, there may be several physically
reasonable local solutions. The development of good nonlinear problem formula-
tions often includes significant effort to provide a reasonable initialization strategy.
Undefined Evaluations
Several nonlinear functions are only well defined over a specific domain (e.g., log(x)
is only valid for x > 0). Therefore, the modeler must take care to ensure the problem
formulation restricts the variable values to be within a valid domain. This is usually
accomplished by setting reasonable bounds and initial values on the variables.
It is also important to note that many nonlinear solvers use first (and sometimes
second) derivative information for the objective function and the constraints. There-
fore, one may need to also restrict the variables to be within a valid domain for the
derivatives of the nonlinear expressions. For example, when sqrt(x) is included √
in an expression, then specifying√ the bounds x ≥ 0 may not be sufficient. While x
is valid at x=0, its derivative, 1/ x is not. This should be considered when setting
reasonable variable bounds.
Finally, note that some nonlinear interior-point solvers (e.g., I POPT) may relax
the variable bounds slightly before solving the problem. While this has proven to
be an effective strategy in many applications, this can sometimes cause a domain
violation even if the modeler has specified reasonable variable bounds. One may
need to disable this behavior in the solver or apply more conservative bounds.
In this section we present several examples that illustrate the capabilities of Pyomo
with nonlinear problems.
which has multiple local minima. The following Pyomo model for this problem
initializes the variables at x=y=0.25.
# multimodal_init1.py
import pyomo.environ as pyo
from math import pi
model = pyo.ConcreteModel()
model.x = pyo.Var(initialize = 0.25, bounds=(0,4))
model.y = pyo.Var(initialize = 0.25, bounds=(0,4))
def multimodal(m):
return (2-pyo.cos(pi*m.x)-pyo.cos(pi*m.y)) * (m.x**2) * \
(m.y**2)
model.obj = pyo.Objective(rule=multimodal, sense=pyo.minimize)
status = pyo.SolverFactory(’ipopt’).solve(model)
pyo.assert_optimal_termination(status)
print(pyo.value(model.x), pyo.value(model.y))
The complete derivation of the sub-population model is given in [5], resulting in the
following set of difference equations,
p
2
fy+1 = p1 bry fy + p3 dy − hyf (7.1)
10
p5
dy+1 = p4 dy + fy − hdy (7.2)
2
p5
by+1 = p6 by + fy − hby (7.3)
2
ps − cy
bry = 1.1 + 0.8 (7.4)
ps
cy = p7 by + p8 dy + p9 fy (7.5)
104 7 Nonlinear Programming with Pyomo
where the value for parameters p1 through p9 are calculated from the various sur-
vival rates and food consumption rates. These values are given in Table 7.3. The
variables fy , dy , and by represent the number of fawns, does, and bucks in year y,
respectively. Likewise, hyf , hdy , and hby are the unknown numbers of fawns, does, and
bucks harvested in year y, respectively. The birth rate bry for does is described by
a nonlinear relationship where cy is the amount of food consumed by the deer (in
pounds) and ps is the total available supply of food (again in pounds).
In the original reference, this set of difference equations was optimized in the
formulation over a period of 20 years so that a sustainable steady-state policy could
be deduced from the values at later years. Here, instead include only one year and
add the constraint that the number of fawns, does, and bucks at year y+1 is equal to
those at y. This provides the same steady-state solution with a significantly smaller
formulation.
The objective is to maximize the value of the harvest, giving the following non-
linear programming formulation,
of consumed food cannot be more than the available supply, thereby restricting the
overall size of the population. Equation (7.13) ensures that the number of bucks is
large enough for effective, sustainable breeding.
The following Pyomo model represents the optimal deer harvesting problem:
# DeerProblem.py
import pyomo.environ as pyo
model = pyo.AbstractModel()
model.p1 = pyo.Param();
model.p2 = pyo.Param();
model.p3 = pyo.Param();
model.p4 = pyo.Param();
model.p5 = pyo.Param();
model.p6 = pyo.Param();
model.p7 = pyo.Param();
model.p8 = pyo.Param();
model.p9 = pyo.Param();
model.ps = pyo.Param();
def obj_rule(m):
return 10*m.hb + m.hd + m.hf
model.obj = pyo.Objective(rule=obj_rule, sense=pyo.maximize)
def f_bal_rule(m):
return m.f == m.p1*m.br*(m.p2/10.0*m.f + m.p3*m.d) - m.hf
model.f_bal = pyo.Constraint(rule=f_bal_rule)
def d_bal_rule(m):
return m.d == m.p4*m.d + m.p5/2.0*m.f - m.hd
model.d_bal = pyo.Constraint(rule=d_bal_rule)
def b_bal_rule(m):
return m.b == m.p6*m.b + m.p5/2.0*m.f - m.hb
model.b_bal = pyo.Constraint(rule=b_bal_rule)
def food_cons_rule(m):
return m.c == m.p7*m.b + m.p8*m.d + m.p9*m.f
model.food_cons = pyo.Constraint(rule=food_cons_rule)
106 7 Nonlinear Programming with Pyomo
def supply_rule(m):
return m.c <= m.ps
model.supply = pyo.Constraint(rule=supply_rule)
def birth_rule(m):
return m.br == 1.1 + 0.8*(m.ps - m.c)/m.ps
model.birth = pyo.Constraint(rule=birth_rule)
def minbuck_rule(m):
return m.b >= 1.0/5.0*(0.4*m.f + m.d)
model.minbuck = pyo.Constraint(rule=minbuck_rule)
instance.pprint()
The optimal solution is to harvest 62 bucks, 37 does, and no fawns. This solution
favors harvesting of bucks, but harvesting too many bucks would affect population
growth. The residual for the minbuck constraint is essentially zero, which means
that this inequality constraint is binding at the solution. Therefore, this constraint is
restricting the number of bucks that can be harvested.
Obviously, this solution is a function of the parameter values that determine the
value of fawns, does, and bucks in the objective function, as well as the parameters
in model for the population dynamics. Because Pyomo is built with Python, it is
straightforward to develop a script to determine the optimal solution as a function of
different parameter values, enabling more advanced analysis of the system. Chapter
5 gives more discussion of this functionality.
7.4 Nonlinear Programming Examples 107
These two difference equations describe the propagation of the disease in the pop-
ulation. As a generation-based model, it is assumed that all the individuals infected
at time i have recovered by time i + 1. Ii and Si are the number of infected and sus-
ceptible individuals at time i, respectively. The population size is given by N, and β
and α are model parameters.
The following listing shows an abstract model for this nonlinear least-squares
estimation problem:
# disease_estimation.py
import pyomo.environ as pyo
model = pyo.AbstractModel()
model.S_SI = pyo.Set(ordered=True)
model.P_REP_CASES = pyo.Param(model.S_SI)
model.P_POP = pyo.Param()
def _objective(model):
return sum((model.eps_I[i])**2 for i in model.S_SI)
model.objective = pyo.Objective(rule=_objective, \
sense=pyo.minimize)
print(’ ***’)
print(’ *** Optimal beta Value: %.2f’ % pyo.value(instance.beta))
print(’ *** Optimal alpha Value: %.2f’ % \
pyo.value(instance.alpha))
print(’ ***’)
The Pyomo data file containing the data for an instance of this model is given by:
# disease_estimation.dat
set S_SI := 1 2 3 4 5 6 7 8 9 10 11 12 13 14
15 16 17 18 19 20 21 22 23 24 25 26 ;
Chemical reactors are often the most important unit operations in a chemical plant.
Reactors come in many forms, however two of the most common idealizations are
the continuously stirred tank reactor (CSTR) and the plug flow reactor. The CSTR
is often used in modeling studies, and it can be effectively modeled as a lumped
parameter system. In this example, we will consider the following reaction scheme
known as the Van de Vusse reaction:
1 k2 k
A→ B→ C
3 k
2A → D
A diagram of the system is shown in Figure 7.2, where F is the volumetric flowrate.
The reactor is assumed to be filled to a constant volume, and the mixture is as-
sumed to have constant density, so the volumetric flowrate into the reactor is equal
to the volumetric flowrate out of the reactor. Since the reactor is assumed to be well-
mixed, the concentrations in the reactor are equivalent to the concentrations of each
component flowing out of the reactor, given by cA , cB , cC , and cD .
F, cAf
Continuously
Stirred F, cA , cB
Tank Reactor
c C , cD
Fig. 7.2: Continuously stirred tank reactor system producing desired product B, and undesired
products C and D from reactant A.
7.4 Nonlinear Programming Examples 111
Consider the following reactor problem adapted from Bequette [7]. The goal is
to produce product B from a feed containing reactant A. If we design a reactor
that is too small, we will obtain insufficient conversion of A to the desired product
B. However, given the reaction scheme, if the reactor is too large (e.g., too much
reaction is allowed to occur), a significant amount of the desired product B will
be further reacted to form the undesired product C. As a result, the goal in this
exercise will be to solve for the optimal reactor volume producing the maximum
outlet concentration for product B.
The steady-state mole balances for each of the four components are given by,
F F
0= cA f − cA − k1 cA − 2k3 c2A
V V
F
0 = − cB + k1 cA − k2 cB
V
F
0 = − cC + k2 cB
V
F
0 = − cD + k3 c2A
V
The known parameters for the system are,
gmol 5 −1 5 −1 1 m3
cA f = 10000 k1 = min k2 = min k 3 = .
m3 6 3 6000 gmol min
Since the volumetric flowrate F always appears as the numerator over the reactor
volume V , it is common to consider this ratio as a single variable, called the space-
velocity (sv). Our optimization formulation will seek to find the space-velocity that
maximizes the outlet concentration of the desired product B.
112 7 Nonlinear Programming with Pyomo
The following file includes a function that builds the concrete model for the re-
actor design problem as well as code that will solve the problem if the Python file is
executed directly:
import pyomo.environ
import pyomo.environ as pyo
return model
if __name__ ==’__main__’:
# solve a single instance of the problem
k1 = 5.0/6.0 # minˆ-1
k2 = 5.0/3.0 # minˆ-1
k3 = 1.0/6000.0 # mˆ3/(gmol min)
caf = 10000.0 # gmol/mˆ3
ample, if we wanted to solve this design problem for different values of the feed
concentration, we could use the following code:
import pyomo.environ as pyo
from ReactorDesign import create_model
# solve the model for different values of caf and report results
print(’{:>10s}\t{:>10s}\t{:>10s}’.format(’CAf’, ’SV’, ’CB’))
for cafi in range(1,11):
caf = cafi*1000.0 # gmol/mˆ3
This script can be executed using the python command, producing the following
results:
CAf SV CB
1000 1.21294 157.564
2000 1.23903 294.346
3000 1.25993 416.943
4000 1.27729 529.051
5000 1.29209 632.993
6000 1.30495 730.339
7000 1.31629 822.212
8000 1.32641 909.447
9000 1.33553 992.687
10000 1.34381 1072.44
This example illustrates the scripting capabilities of Pyomo. See Chapter 5 for ad-
ditional scripting examples and further description of these capabilities.
Chapter 8
Structured Modeling with Blocks
8.1 Introduction
min cT x
s.t. Ax ≤ b .
x≥0
Here, the variables are lumped together into a single vector x, and constraints are
represented in simplified matrix form. While this form is convenient for algorithms
directly manipulating these matrices, it is not an easy form for a modeler to gener-
ate, manipulate, or debug. Algebraic Modeling Languages (AMLs) directly address
this challenge by allowing modelers to provide distinguishing names to modeling
components (e.g., variables or constraints) and to define the model over index sets.
Since models are often composed of repeated mathematical expressions, this allows
the expression of large models with relatively few lines of code, which are also
easier to document, understand, modify, and debug.
As models become larger and more complex, we often want to carry this con-
cept further using the principle of composition from objet-oriented programming.
115
116 8 Structured Modeling with Blocks
In this approach, we group - or compose - variables and constraints that are con-
ceptually related into a single object. This modeling approach, does not emphasize
that the constraints are connected by a common expression generator, but rather the
variables and constraints describe a certain (often physical) concept. For example,
the group of variables and constraints could represent the operating behavior of an
electric generator (ramp-up limits, ramp-down limits, cost curves) or chemical pro-
cess equipment like a distillation column (the mass, equilibrium, and energy balance
equations). Other examples include multi-period optimization problems where the
same fundamental model is repeated over many time periods, or stochastic program-
ming problems where the same basic model is repeated over different scenarios with
different parameters. In Pyomo, we use the Block component to support general
composition of modeling components like those described.
The Block component is a container for organizing groups of variables and
constraints, and it can contain any number of named Pyomo components in exactly
the same way models do. In fact, ConcreteModel and AbstractModel are
themselves special implementations of the Block component. Block components
can be added to a model or another block, allowing modelers to construct hierar-
chical model structures based on fundamental building blocks in an object-oriented
manner.
This concept is illustrated in Figure 8.1, which shows one possible block-oriented
representation of a an electrical grid model. In this example, individual blocks define
Electrical grid
model (Block)
Electrical grid
model (Block)
Electrical grid
model (Block)
…
Generator model
(Block)
Electrical grid model
Fig. 8.1: The electrical grid model can be composed from individual blocks representing the
generators, buses, and transmission lines. Furthermore, a multi-period model can be constructed in
a hierarchical manner treating the electrical grid model as its fundamental building blocks.
8.2 Block structures 117
The Pyomo Block component can be treated in much the same way as a model:
components are added directly to the block as attributes. Since Block components
may contain any other Pyomo modeling components, including other blocks, it is
possible to construct arbitrarily nested hierarchical structures.
The following code snippet shows a basic block hierarchy:
model = pyo.ConcreteModel()
model.x = pyo.Var()
model.P = pyo.Param(initialize=5)
model.S = pyo.RangeSet(model.P)
model.b = pyo.Block()
model.b.I = pyo.RangeSet(model.P)
model.b.x = pyo.Var(model.b.I)
model.b.y = pyo.Var(model.S)
model.b.b = pyo.Block([1,2])
model.b.b[1].x = pyo.Var()
model.b.b[2].x = pyo.Var()
ponents and expressions can reference components from anywhere within the hier-
archy.
NOTE: Within one block, Pyomo supports references to components from any
other block. However, it is generally good object-oriented practice to only ref-
erence components from the current or lower levels in the hierarchy. This pro-
motes resusability of your blocks within other models without strong assump-
tions about the structure of the owning or parent blocks.
Note in the code snippet each block defines its own component namespace; while
component names must be unique within a single block, they do not need to be
globally unique. This allows blocks to be constructed safely without concerns about
definitions in one block colliding or interfering with definitions in other blocks. This
leads to all components having two forms of a name: the local name, which may be
repeated elsewhere in the model, and a globally-unique fully qualified name that
includes the names of the parent block(s) separated by periods:
print(model.x.local_name) # x
print(model.x.name) # x
print(model.b.x.local_name) # x
print(model.b.x.name) # b.x
print(model.b.b[1].x.local_name) # x
print(model.b.b[1].x.name) # b.b[1].x
When discussing the block hierarchy, we adopt terminology from tree structures
and refer to the block one level up the hierarchy (toward the top-level model) as the
parent block, and all components contained within a block as its children. The root
of the block hierarchy is always the current model. Pyomo components all provide
a set of standard methods for moving around the component hierarchy:
parent component() Each modeling object in a Pyomo model is owned by
a single component. Calling parent component() on a modeling object
returns the component that holding (or owning) the modeling object. For mem-
bers of an indexed component, parent component() returns the indexed
component object containing the component. For scalar components, the parent
component is the scalar component itself.
parent block() Each modeling component is attached to a single block. Call-
ing parent block() on a Pyomo modeling object returns the block that
owns the object’s parent component().
model() Calling model() on a modeling object walks up the parent block()
calls to return the top-level (root) block.
getitem As with models, child components can be accessed programmati-
cally by attribute lookup on the block.
component() Child components can also be retrieved by name using the Blocks
component() method.
8.3 Blocks as Indexed Components 119
Block components can also be created and populated, and then later added to a
model. The code below shows creation of a block that is later added to a model. It
also illustrates how a parent model can make use of sets and parameters contained
in a child block.
new_b = pyo.Block()
new_b.x = pyo.Var()
new_b.P = pyo.Param(initialize=5)
new_b.I = pyo.RangeSet(10)
model = pyo.ConcreteModel()
model.b = new_b
model.x = pyo.Var(model.b.I)
NOTE: In this example, the Block object new b is not initialized when it is
declared. In this manner, it is abstract until it is added to the ConcreteModel
object. At this point, it is immediately initialized. Similarly, when a Block
object is added to an AbstractModel, it is not initialized until the owning
abstract model object is initialized.
As with other Pyomo components, Block components may also be indexed and
initialized using a construction rule. However, block construction rules follow a
slightly different convention: the first argument to a block rule is the block to be
populated rather than the owning block. This block has already been attached to the
model so methods like model() and parent block() will work as expected.
Within a rule, one can either directly populate the block by assigning components
to it, or create a new block and return it from the rule.
120 8 Structured Modeling with Blocks
The following example illustrates the use of construction rules for blocks:
model = pyo.ConcreteModel()
model.P = pyo.Param(initialize=3)
model.T = pyo.RangeSet(model.P)
Here, an indexed Block component containing one block for each element of the
set model.T is defined. In the construction rule, we create two variables and a
set. These construction rules are just as flexible as those for other components. The
set b.I created in this example is different for each block, and consequently the
variable b.y is also a different length for each block. This illustrates another feature
of blocks. Often, different blocks contain exactly the same model structure, just with
different data. It is also possible to construct blocks with a different structure based
on data available in the construction rule.
This example can be extended to print the constraint body c for each of the
blocks:
for t in model.T:
print(model.xyb[t].c.body)
The constraints expand appropriately and they contain fully qualified names of vari-
ables in each of the subblocks:
-1.0 + xyb[1].x + xyb[1].y[1]
-1.0 + xyb[2].x + xyb[2].y[1] + xyb[2].y[2]
-1.0 + xyb[3].x + xyb[3].y[1] + xyb[3].y[2] + xyb[3].y[3]
Like model objects, blocks can contain other modeling components, including Set
and Param objects. Additionally, blocks can be initialized with modeling compo-
nents that are themselves constructed using rules. However, doing this exposes a
subtelty of Pyomo component construction rules.
Up to this point we have frequently referred to the first argument of a compo-
nent rule as the “model”, but this is not completely correct. The first argument to
component rules is actually the owning block of the component being constructed.
For “flat” models (models without any sub-blocks), the owning block is indeed the
model, but this will not be the case for hierarchically-structured models. If needed,
the model object can be obtained from the owning block using the aforementioned
model() method.
8.6 Blocks Example: Optimal Multi-Period Lot-Sizing 121
For example, consider the following alternative declaration of the xyb block
from the previous example:
def xyb_rule(b, t):
b.x = pyo.Var()
b.I = pyo.RangeSet(t)
b.y = pyo.Var(b.I, initialize=1.0)
def _b_c_rule(_b):
return _b.x == 1.0 - sum(_b.y[i] for i in _b.I)
b.c = pyo.Constraint(rule=_b_c_rule)
model.xyb = pyo.Block(model.T, rule=xyb_rule)
In this example, the xyb block includes a constraint that is defined with the rule
b c rule. The owning block b passed to this rule is the same as b. However,
the b variable is defined locally. This allows the rule to be used even if the owning
block is constructed in a different manner.
Since the owning block (or model) is NOT passed into a block construction
rule, the modeler may need another mechanism to access components on the par-
ent or other blocks in the hierarchy. The component methods parent block and
model facilitate moving up the block hierarchy. The parent block() of any
component or component data object is the block that the component is attached
to. The model() method on any component or component data object returns the
block object at the root of the tree.
It = It−1 + Xt − dt .
If we allow the inventory to be negative (meaning we did not meet demands and we
have a backlog of orders), we can represent the inventory as It =It+ −It− where we
restrict both It+ and It− to be non-negative. Here, It+ represents inventory that we are
holding, and It− represents a backlog of orders. We can assign an inventory holding
cost and a shortage cost (cost of keeping a backlog) as ht+ and ht− , respectively.
With this description, the optimization problem can be formulated as,
where Equation (LS.4) is a constraint that only allows production in time period t if
the indicator variable yt =1. The data for our problem is provided in Table 8.1.
We can formulate the lot-sizing problem without blocks using the Pyomo code
shown below.
import pyomo.environ as pyo
model = pyo.ConcreteModel()
model.T = pyo.RangeSet(5) # time periods
This example uses standard Pyomo syntax as discussed in early chapters of the
book. If we were considering the lot-sizing problem over a single time period only,
our variable declarations would have looked like,
124 8 Structured Modeling with Blocks
In the multi-period case, we have the same fundamental variables and constraints
defined over each time period. Here, the variable declarations looked like,
# define the variables
model.y = pyo.Var(model.T, domain=pyo.Binary)
model.x = pyo.Var(model.T, domain=pyo.NonNegativeReals)
model.i = pyo.Var(model.T)
model.i_pos = pyo.Var(model.T, domain=pyo.NonNegativeReals)
model.i_neg = pyo.Var(model.T, domain=pyo.NonNegativeReals)
We now show how blocks can be used to write this problem. Most of the constraints
in the multi-period lot-sizing problem are defined over t ∈ T , and they can be log-
ically grouped together by time. Pyomo allows us to define blocks, each with the
variables and constraints for a single time period only, and then link them together
to form the overall model.
Considering the lot-sizing problem again, the variables and constraints within a
rule can be defined to provide a block for a single period of the lot-sizing problem:
8.6 Blocks Example: Optimal Multi-Period Lot-Sizing 125
Here, the variables and constraints for a single time period t are defined within
the rule. The Block component is then indexed over the set model.T, and the
declaration constructs a lot-sizing block for each entry in model.T. The model
object now contains a block for each time period t. The final action is to provide con-
straints linking the blocks together (setting the initial inventory of one block equal
to the final inventory of the previous block), and to define the objective function
over all the blocks. The full code listing for the block version of the multi-period
lot-sizing problem is shown below.
126 8 Structured Modeling with Blocks
model = pyo.ConcreteModel()
model.T = pyo.RangeSet(5) # time periods
This formulation is small, so it can be difficult to see the benefit of blocks. How-
8.6 Blocks Example: Optimal Multi-Period Lot-Sizing 127
ever, as models grow in size and complexity, this object-oriented modeling concept
allows us to define small pieces of the model in self-contained chunks of code, and
then build the large model by pulling these pieces together. This example was se-
lected in part because it is a heavily studied, classic multi-stage inventory model.
One can easily imagine extensions to the model to include additional constraints
and costs. In fact, many such models have appeared in the academic literature and
in practical application. In large models, it is common to write methods or classes
to define individual blocks and reuse code within several different, high-level opti-
mization formulations.
Chapter 9
Performance: Model Construction and Solver
Interfaces
Abstract This chapter documents tools for profiling model construction and im-
proving the performance of both model construction and interaction with solvers.
We begin by discussing various profiling tools which can be used to help identify
performance bottlenecks. Pyomo has built-in profiling capabilities, but there are also
Python packages, such as cProfile and line profiler, dedicated to perfor-
mance profiling. Section 9.2 discusses the LinearExpression class, which can be
used to drastically improve model construction time for some applications. Section
9.3 describes how persistent solver interfaces can be used to repeatedly solve mod-
els with small changes very efficiently. Finally, Section 9.4 discusses sparse index
sets.
129
130 9 Performance: Model Construction and Solver Interfaces
model = pyo.ConcreteModel(name="(WL)")
model.P = pyo.Param(initialize=max_num_warehouses,
mutable=True)
def obj_rule(mdl):
return sum(d[n,m]*mdl.x[n,m] for n in N for m in M)
model.obj = pyo.Objective(rule=obj_rule)
def num_warehouses_rule(mdl):
return sum(mdl.y[n] for n in N) <= model.P
model.num_warehouses = \
pyo.Constraint(rule=num_warehouses_rule)
return model
9.1 Profiling to Identify Performance Bottlenecks 131
Pyomo has a very useful function, report timing, for profiling model construc-
tion. The following illustrates how to use report timing to profile the construc-
tion of the warehouse location problem.
report_timing()
print(’Building model’)
print(’--------------’)
m = create_warehouse_model(num_locations=200, num_customers=200)
Calling the report timing function causes Pyomo to print the time required to
build each component. In the second to last line in the output, we can see build-
ing the warehouse active constraint accounts for the majority of the model
construction time. Note that any data processing done inside the constraint rules is
included, so not all of the time is necessarily spent in Pyomo. In this example, there
is not any data processesing within the rule, so we know building the expressions
for the warehouse active constraint is the bottleneck.
132 9 Performance: Model Construction and Solver Interfaces
9.1.2 TicTocTimer
Pyomo also provides a TicTocTimer for convenient timing. In the following ex-
ample, we compare the time required to build the model and the time to write an LP
file and solve the problem with Gurobi.
def solve_warehouse_location(m):
opt = pyo.SolverFactory(’gurobi’)
res = opt.solve(m)
assert_optimal_termination(res)
timer = TicTocTimer()
timer.tic(’start’)
m = create_warehouse_model(num_locations=200, num_customers=200)
timer.toc(’Built model’)
solve_warehouse_location(m)
timer.toc(’Wrote LP file and solved’)
The output states it took 1.22 seconds to build the Pyomo model and a total of 4.05
seconds to write the LP file and solve the problem. By utilizing the TicTocTimer,
we found that writing the LP file and solving the problem took significantly longer
than constructing the model. However, it is not yet clear what fraction of the 4.05
seconds is spent writing the LP file, which is where cProfile is useful.
9.1 Profiling to Identify Performance Bottlenecks 133
9.1.3 Profilers
We first call the solve parametric function and report the time required to
perform the parameter sweep.
solve_parametric()
timer.toc(’Finished parameter sweep’)
The TicTocTimer reports that it took 7.28 seconds to complete the parameter
sweep. Next, we write a function to help print the output from cProfile.
def print_c_profiler(pr, lines_to_print=15):
s = io.StringIO()
stats = pstats.Stats(pr, stream=s).sort_stats(’cumulative’)
stats.print_stats(lines_to_print)
print(s.getvalue())
s = io.StringIO()
stats = pstats.Stats(pr, stream=s).sort_stats(’tottime’)
stats.print_stats(lines_to_print)
print(s.getvalue())
We use the pstats package to sort the output from cProfile and print the
specified number of lines. We print the statistics sorted by both cumulative time and
total time. As described in the cProfile documentation, the cumulative time is the
time spent in the corresponding function, including all functions called within the
function. The total time is the time spent in the corresponding function, excluding
all calls to functions within the specified function.
We can use the cProfile package as follows.
134 9 Performance: Model Construction and Solver Interfaces
pr = cProfile.Profile()
pr.enable()
solve_parametric()
pr.disable()
print_c_profiler(pr)
The first line of output shows the total number of function calls and the total time
required to run the profiled code. Note the 10.229 seconds reported by cProfile
is significantly longer than the 7.28 seconds reported by the TicTocTimer. Us-
ing cProfile does add some overhead to the code being profiled. Therefore,
cProfile should be used to identify bottlenecks (rather than comparing two al-
gorithms, for example). The first block of output is sorted by cumulative time. Each
row shows the cumulative time spent in the function on the far right of the row. As
expected, the entire 10.229 seconds is spent within the solve parametric func-
tion. Of the total 10.229 seconds, 10.107 seconds are spent within the Pyomo call
to solve. We can already conclude very little time is spent constructing the model,
changing the value of m.P, and checking the termination condition of the solver.
Additionally, we see the call to write (which is where the LP file is written) takes
5.675 seconds of the 10.107 seconds spent in solve. Only 3.528 seconds are spent
in apply solver, which is where the subprocess command that calls Gurobi is
executed. These results are an indication a persistent solver interface would be use-
9.2 Improving Model Construction Performance with LinearExpression 135
ful for this application. The persistent solver interfaces will be discussed in section
9.3.
N1 = 10
N2 = 100000
m = pyo.ConcreteModel()
m.x = pyo.Var(list(range(N1)))
timer = TicTocTimer()
timer.tic()
for i in range(N2):
e = sum(i*m.x[i] for i in range(N1))
timer.toc(’created expression with sum function’)
for i in range(N2):
coefs = [i for i in range(N1)]
lin_vars = [m.x[i] for i in range(N1)]
e = LinearExpression(constant=0, linear_coefs=coefs, \
linear_vars=lin_vars)
timer.toc(’created expression with LinearExpression constructor’)
Although using the LinearExpression is less clear and less concise, it is over
6 times faster in this example. Note the performance improvement depends heavily
on the number of terms in the expression.
Each time a Pyomo model is solved using the standard solver interfaces, the gener-
alized modeling components defining the model must be translated into some input
form recognized by the solver in use. For cases where the solver finishes relatively
quickly, this model translation may introduce significant timing overhead. Persis-
tent solver interfaces provide a mechanism for reducing overall translation time for
models that are solved repeatedly with incremental changes. This is made possible
by exposing functionality allowing users to efficiently notify the solver of these in-
cremental changes to the Pyomo model, after an initial step where the full model is
translated.
When a persistent interface is used, additional care must be taken so that the Py-
omo model and its translated solver representation are kept in sync. This is a manual
process that must be done by the user. However, with this tradeoff, significant per-
formance improvements are possible for many common optimization approaches.
In Section 9.3.1, we briefly discuss when persistent solvers are most useful. In
Sections 9.3.2 – 9.3.4, we describe how to use a persistent solver interface. In Sec-
tion 9.3.5, we re-implement the parametric sweep example from Section 9.1.3 using
a persistent solver interface.
Persistent solver interfaces are designed to be used when repeatedly solving the
same model with minor changes. They are most useful when the solver time is not
significantly longer than the time needed for Pyomo to translate the model to the
solver’s input format. Of course, persistent solvers can be used no matter how long
it takes to solve the problem. However, if the time spent solving the problem is
significantly longer than the time needed for translation, then little speedup will be
observed, but the complexity of the code may have increased. Linear programs are
excellent candidates for use with persistent solver interfaces because most linear
programs can be solved very efficiently. On the other hand, many mixed-integer
programs are difficult to solve and are poor candidates for the persistent solver in-
terfaces. Ultimately, it is necessary to use the profilers discussed in Section 9.1 to
determine how beneficial a persistent solver interface will be for any particular ap-
plication.
9.3 Repeated Solves with Persistent Solvers 137
The first step in using a persistent solver is to create a Pyomo model as usual.
import pyomo.environ as pyo
m = pyo.ConcreteModel()
m.x = pyo.Var()
m.y = pyo.Var()
m.obj = pyo.Objective(expr=m.x**2 + m.y**2)
m.c = pyo.Constraint(expr=m.y >= -2*m.x + 5)
This will create a gurobipy Model object and include the appropriate variables and
constraints. We can now solve the model.
results = opt.solve()
Note that the model should not be passed into the solve method, as is done with
most solver interfaces. We can also add or remove variables, constraints, or blocks
and set objectives. For example,
m.c2 = pyo.Constraint(expr=m.y >= m.x)
opt.add_constraint(m.c2)
This tells the solver to add one new constraint but otherwise leave the model un-
changed. We can now resolve the model.
results = opt.solve()
If a Pyomo component is replaced with another component with the same name,
the first component must be removed from the solver. Otherwise, the solver will
have multiple components. For example, the following code will run without error,
but the solver will have an extra constraint. The solver will have both y ≥ −2 ∗ x + 5
and y ≤ x, which is not what was intended!
m = pyo.ConcreteModel()
m.x = pyo.Var()
m.y = pyo.Var()
m.c = pyo.Constraint(expr=m.y >= -2*m.x + 5)
opt = pyo.SolverFactory(’gurobi_persistent’)
opt.set_instance(m)
# WRONG:
del m.c
m.c = pyo.Constraint(expr=m.y <= m.x)
opt.add_constraint(m.c)
In most cases, the only way to modify a component is to remove it from the
solver instance, modify it with Pyomo, and then add it back to the solver instance.
The only exception is with variables. Variables may be modified and then updated
with the solver:
m = pyo.ConcreteModel()
m.x = pyo.Var()
m.y = pyo.Var()
m.obj = pyo.Objective(expr=m.x**2 + m.y**2)
m.c = pyo.Constraint(expr=m.y >= -2*m.x + 5)
opt = pyo.SolverFactory(’gurobi_persistent’)
opt.set_instance(m)
m.x.setlb(1.0)
opt.update_var(m.x)
In short, any time the Pyomo model is changed, the persistent solver interface
must be notified and kept in sync. Table 9.1 presents the appropriate methods to
use for various Pyomo model modifications. Note that when mutable parameters or
named Expressions are modified, all constraints utilizing the modified parame-
ters/expressions must be updated (removed and re-added).
9.3 Repeated Solves with Persistent Solvers 139
The examples in section 9.3.2 all used scalar variables and constraints; in order to
use indexed variables and/or constraints, the code must be slightly adapted:
m.v = pyo.Var([0, 1, 2])
m.c2 = pyo.Constraint([0, 1, 2])
for i in range(3):
m.c2[i] = m.v[i] == i
for v in m.v.values():
opt.add_var(v)
for c in m.c2.values():
opt.add_constraint(c)
This must be done when removing indexed variables and constraints, too. Note that
the is indexed method can be used to automate the process.
140 9 Performance: Model Construction and Solver Interfaces
In order to get the best performance out of the persistent solvers, use the
save results argument:
m = pyo.ConcreteModel()
m.x = pyo.Var()
m.y = pyo.Var()
m.obj = pyo.Objective(expr=m.x**2 + m.y**2)
m.c = pyo.Constraint(expr=m.y >= -2*m.x + 5)
opt = pyo.SolverFactory(’gurobi_persistent’)
opt.set_instance(m)
results = opt.solve(save_results=False)
Note that if the save results flag is set to False, then the following is not
supported.
results = opt.solve(save_results=False, load_solutions=False)
if results.solver.termination_condition == \
pyo.TerminationCondition.optimal:
try:
m.solutions.load_from(results)
except AttributeError:
print(’AttributeError was raised’)
Additionally, a subset of variable values may be loaded back into the model:
results = opt.solve(save_results=False, load_solutions=False)
if results.solver.termination_condition == \
pyo.TerminationCondition.optimal:
opt.load_vars([m.x])
9.3 Repeated Solves with Persistent Solvers 141
9.3.5 Example
In this section, we re-implement the parameter sweep example from Section 9.1.3
using a persistent solver interface. The following is a function which performs the
parameter sweep with a persistent solver interface.
def solve_parametric_persistent():
m = create_warehouse_model(num_locations=50, num_customers=50)
opt = pyo.SolverFactory(’gurobi_persistent’)
opt.set_instance(m)
p_values = list(range(1, 31))
obj_values = list()
for p in p_values:
m.P.value = p
opt.remove_constraint(m.num_warehouses)
opt.add_constraint(m.num_warehouses)
res = opt.solve(save_results=False)
assert_optimal_termination(res)
obj_values.append(res.problem.lower_bound)
There are a few additional lines of code compared to the function from Section 9.1.3.
Specifically, we have added calls to the set instance, remove constraint,
and add constraint methods on the solver interface.
The following code block calls the above function and reports the execution time.
timer.tic()
solve_parametric_persistent()
timer.toc(’Finished parameter sweep with persistent interface’)
Note this function was approximately 8 times faster than the non-persistent coun-
terpart (0.91 seconds vs 7.28 seconds). Recall that the performance depends heavily
on the application as discussed in Section 9.3.1.
142 9 Performance: Model Construction and Solver Interfaces
Often, a model makes use of only a portion of the cross product of multiple index
sets. In particular, sometimes the members in one dimension depend on the value in
another, leading to the slang “jagged sets.”
For example, a modeler may want to have a constraint to hold for
i ∈ I , k ∈ K , v ∈ Vk .
There are many ways to accomplish this, but one good way is to create a set of
tuples composed of all of valid “model.k, model.V[k]” pairs. This is illustrated in
the following example where the jagged set KV is used.
import pyomo.environ as pyo
model = pyo.AbstractModel()
model.I = pyo.Set()
model.K = pyo.Set()
model.V = pyo.Set(model.K)
def kv_init(m):
return ((k,v) for k in m.K for v in m.V[k])
model.KV = pyo.Set(dimen=2, initialize=kv_init)
model.y = pyo.Var(model.I)
model.x = pyo.Var(model.I, model.KV)
def c1Rule(m,i,k,v):
return m.x[i,k,v] <= m.a[i,k]*m.y[i]
model.c1 = pyo.Constraint(model.I, model.KV, rule=c1Rule)
Abstract This chapter describes how to declare and use an AbstractModel and
data command files to initilize abstract models. Finally, this chapter describes the
pyomo command, which makes it particularly easy to solve an abstract model using
data command files. Although concrete and abstract models provide similar func-
tionality, abstract models make a strong seperation of model formulation and model
data, which is conceptually nice and practically useful in some contexts.
10.1 Overview
In many of the examples in this book, we use a function that takes in data and
returns a ConcreteModel. This facilitates separating the concepts of model and
data. The AbstractModel class in Pyomo provides this separation by populating
the model with data only after the abstract model object has been created.
Pyomo supports two strategies for model declaration: concrete models, which im-
mediately construct model components, and abstract models, which defer compo-
nent construction. Abstract models reflect the structure of many mathematical op-
timization formulations. For example, the formulation of the warehouse location
problem (WL) on page 28 is written in a general manner describing a class of opti-
mization problems. However, we cannot solve this problem because the actual data
for the problem (N, M, d, and P) have not been specified. A solver must be given a
specific instance of a problem (with the data specified).
In Pyomo, an abstract model is declared first, and component construction is de-
layed until the data is loaded and Pyomo creates the model instance. This modeling
approach is illustrated in the top pane of Figure 10.1. An AbstractModel object
143
144 10 Abstract Models and Their Solution
is created, and then the data for a particular problem is given to Pyomo, and then
Pyomo performs the construction process in order to create an instance of the model
with all the variables, constraint expressions, and objective expressions that can be
sent to a solver. This requires a two-pass approach where the model is declared in
the first pass, and subsequently the model is constructed using data values speci-
fied separately. To support delayed construction, the model must be defined using
construction rules.
By contrast, concrete models support a programmatic style where the model in-
stance is created immediately; model components are constructed and initialized on
the first pass as Python executes the model script. This modeling approach is illus-
trated in the bottom pane of Figure 10.1. A ConcreteModel object is created, and
the data needs to be present before each component is declared. As Python executes
your model script, the particular model instance and its components are created
immediately as Python encounters the component declaration. Once the execution
through the Python file is completed the model is ready to be sent to the solver (i.e.,
a single pass). At this point, the ConcreteModel is the specific instance.
NOTE: Construction rules can be still be used with concrete models (the rules
are immediately fired as they are encountered in the model’s Python file).
data
data
ConcreteModel
data solver
ConcreteModel
(instance)
data solver
(instance)
Fig. 10.1: This figure describes the construction process for both abstract and concrete models.
The top pane describes the declarative style used for abstract models. The AbstractModel is
first created. Then, given a particular realization of the data, Pyomo performs the construction
process in order to create an instance of the model that can be sent to the solver. The bottom pane
describes the programmatic style used for concrete models. As Python executes the model script,
the component objects are constructed immediately using data previously declared. The particular
model instance is ready to be sent to the solver once the first pass is complete.
10.1 Overview 145
Consider model (H) (see page 19), which is reproduced here for convenience:
Since model (H) is an abstract model, a natural way of expressing this model in
Pyomo is with Pyomo’s AbstractModel class.
146 10 Abstract Models and Their Solution
model = pyo.AbstractModel(name="(H)")
model.A = pyo.Set()
model.h = pyo.Param(model.A)
model.d = pyo.Param(model.A)
model.c = pyo.Param(model.A)
model.b = pyo.Param()
model.u = pyo.Param(model.A)
def obj_rule(model):
return sum(model.h[i] * \
(model.x[i] - (model.x[i]/model.d[i])**2) \
for i in model.A)
model.z = pyo.Objective(rule=obj_rule, sense=pyo.maximize)
def budget_rule(model):
return sum(model.c[i]*model.x[i] for i in model.A) <= model.b
model.budgetconstr = pyo.Constraint(rule=budget_rule)
Given particular data for the parameters in this model, then one might be inter-
ested in finding an optimal assignment of values to model.x. There are a variety
of ways to provide data to Pyomo for an abstract model. Here is data (saved in
AbstractH.dat) defining a suitable happiness objective for one of the authors
of this book:
# Pyomo data file for AbstractH.py
set A := I_C_Scoops Peanuts ;
param h := I_C_Scoops 1 Peanuts 0.1 ;
param d :=
I_C_Scoops 5
Peanuts 27 ;
param c := I_C_Scoops 3.14 Peanuts 0.2718 ;
param b := 12 ;
param u := I_C_Scoops 100 Peanuts 40.6 ;
This is a Pyomo data file, which includes set and param commands closely re-
sembling AMPL data commands. Description of these data commands is given in
Section 10.3.
The following lines can be used to optimize an abstract model, by adding them
to the Python file defining the model:
10.1 Overview 147
opt = pyo.SolverFactory(’glpk’)
instance = model.create_instance("AbstractH.dat")
results = opt.solve(instance) # solves and updates instance
instance.display()
Alternatively, we can also solve the model using the pyomo command as described
in Section 10.2
The warehouse location problem (see Section 3.2) can be represented as an abstract
model as follows:
The sets are declared as Pyomo Set components and the parameter data is de-
148 10 Abstract Models and Their Solution
clared as Pyomo Param components with no indication as to how the data will be
supplied. Pyomo is informed the Param and Var components will be indexed by
sets, but the contents of those sets have not been declared.
The objective construction rule is defined on as the Python function obj rule
and then model.obj is declared to be Pyomo Objective component. Since
this is an abstract model, the objective rule obj rule is not yet called. At this
point, Pyomo knows what rule to call to construct the objective component, but it
has not called the constructor because this is an abstract model. Constraint rules and
constraint components are declared in a similar manner.
Data can be expressed in several different formats. For example, the following
Pyomo data file can be used:
# wl_data.dat: Pyomo format data file for the warehouse \
location problem
param d :=
Harlingen NYC 1956
Harlingen LA 1606
Harlingen Chicago 1410
Harlingen Houston 330
Memphis NYC 1096
Memphis LA 1792
Memphis Chicago 531
Memphis Houston 567
Ashland NYC 485
Ashland LA 2322
Ashland Chicago 324
Ashland Houston 1236
;
param P := 2 ;
The script can be executed with the python command, but this action would
not actually do anything. This script declares the model, but it does not define the
model data or create the problem instance for the solver. The action of applying a
data file to this abstract model can be scripted explicitly in Python code, or it can be
done using the pyomo command. For example:
pyomo solve --solver=glpk wl_abstract.py wl_data.dat
The --summary flag can be used to provide more detailed output about the solu-
tion.
When pyomo runs, it executes wl abstract.py to create an
AbstractModel with the name model. This model object contains the Pyomo
modeling components that have been declared. Then pyomo reads the data file
wl data.dat and applies this data to the Set and Param components in the
same order that the components were declared in the model. Next, the pyomo com-
mand constructs all of the remaining components in declaration order: the variables,
10.2 The pyomo Command 149
the objective, and the constraints. After the model is constructed, pyomo calls the
solver to find the solution.
Abstract models can be used in scripts, but a concrete instance must be created
from the AbstractModel object using the create instance method. The
following example takes an AbstractModel, constructs the instance using a data
file called wl data.dat, solves the instance, and prints some results:
instance = model.create_instance(’wl_data.dat’)
solver = pyo.SolverFactory(’glpk’)
solver.solve(instance)
instance.y.pprint()
If you don’t want to write your own script, but instead want to use the pyomo
command, then you should not add these lines.
The Pyomo software distribution includes a pre-defined execution script, the pyomo
command, that includes a variety of subcommands supporting use of Pyomo. The
following subcommands are supported in Pyomo 6.0:
check
This subcommand checks a model for errors. This is particularly useful for eval-
uating the logic of rules in abstract models.
convert
This subcommand is used to convert a Pyomo model into another format, such
as an lp or nl file.
help
Print information about the configuration and installation of Pyomo. For ex-
ample, the -s option provides information about available solvers:
pyomo help -s
run
Execute a command from the Pyomo bin (or Scripts) directory. For example,
this provides a handy mechanism for launching Python with Pyomo installed:
pyomo run python
solve
Construct and optimize a model.
test-solvers
Execute a variety of tests to verify solver capabilities.
The following sections illustrate the use of the check, convert, help and
solve subcommands, which can be customized with a variety of options.
150 10 Abstract Models and Their Solution
The model construction step requires a Pyomo A Pyomo model file, which is
a Python file defining a Pyomo model object. Thus, the solve subcommand can
be viewed as a generic script for analyzing a model defined by a Pyomo model
file. The solve subcommand has a variety of optional command-line arguments to
customize the optimization process; documentation of the various available options
is available by specifying the --help option.
However, the solve subcommand can also be executed with a YAML or JSON
configuration file1 , which eliminates the need to specify command-line options.
Consider the following configuration file:
# concrete1.yaml
model:
filename: concrete1.py
solvers:
- solver name: glpk
This configuration file can be used to configure the executions of the pyomo sub-
command as follows:
pyomo solve concrete1.yaml
This configuration file defines the same logic as the first command in the previous
paragraph, and the following configuration file defines the same logic as the second
command:
# abstract5.yaml
model:
filename: abstract5.py
data:
files:
- abstract5.dat
solvers:
- solver name: glpk
No command-line options are required when using a configuration file, because all
command-line options have corresponding elements in a configuration file. Further-
more, there are configuration options that can only be expressed in a configuration
file. A template configuration file can be generated with the
--generate-config-template option .
1 YAML and JSON are data serialization standards. JSON is supported natively in Python, and
information about JSON is available at www.json.org. YAML configuration files are supported
if the PyYAML package is installed, and information about YAML is available at www.yaml.org.
152 10 Abstract Models and Their Solution
Option Description
-c, --catch-errors Trigger failures for exceptions and print the program stack.
--json Store results in JSON format.
-k, --keepfiles Keep temporary files.
-l, --log Print the solver logfile after performing optimization.
--logfile FILE Redirect output to the specified file.
--logging LEVEL Specify the logging level: quiet, warning, info, verbose, de-
bug.
--model-name NAME The name of the model object created in the specified Py-
omo module.
--path PATH Give a path used to find Pyomo Python files.
--report-timing Report various timing statistics during model construction.
--results-format FORMAT Specify the results format: json or yaml.
--save-results FILE The filename to which the results are saved.
--show-results Print the results object after optimization.
--solver SOLVER Specify the solver name.
--solver-executable FILE The executable used by the solver interface.
--solver-io FORMAT The type of IO used to execute the solver. Different solvers
support different types of IO, but the following are com-
mon options: lp - generate LP files, nl - generate NL files,
python - direct Python interface.
--stream-output Stream the solver output to provide information about the
solver’s progress.
--solver-options STRING String describing solver options.
--solver-suffix SUFFIXES Solution suffixes that will be extracted by the solver (e.g.,
rc, dual, or slack).
--summary Summarize the final solution after performing optimiza-
tion.
--symbolic-solver-labels When interfacing with the solver, use symbol
names derived from the model. For example,
”my special variable[1 2 3]” instead of ”v1”. When
using the ASL solvers, this option generates correspond-
ing .row (constraints) and .col (variables) files.
--tempdir TEMPDIR Specify the directory where temporary files are generated.
Table 10.1: Commonly used options for the pyomo solve subcommand.
Table 10.1 summarizes key options for the solve subcommand that are com-
monly used.
10.2 The pyomo Command 153
A model file can execute an arbitrary Python script, but the expectation of the
pyomo solve command is that it generates an object that contains the Pyomo
model. Within the solve subcommand, a model file is executed with a Python
import command, and thus it is interpreted like any other Python file.
In the simplest case, a Pyomo model file contains Python commands to create
a model object stored in the model variable. For example, consider the following
simple LP:
# abstract5.py
import pyomo.environ as pyo
model = pyo.AbstractModel()
model.N = pyo.Set()
model.M = pyo.Set()
model.c = pyo.Param(model.N)
model.a = pyo.Param(model.N, model.M)
model.b = pyo.Param(model.M)
def obj_rule(model):
return sum(model.c[i]*model.x[i] for i in model.N)
model.obj = pyo.Objective(rule=obj_rule)
Model = pyo.AbstractModel()
Model.N = pyo.Set()
Model.M = pyo.Set()
Model.c = pyo.Param(Model.N)
Model.a = pyo.Param(Model.N, Model.M)
Model.b = pyo.Param(Model.M)
def obj_rule(Model):
return sum(Model.c[i]*Model.x[i] for i in Model.N)
Model.obj = pyo.Objective(rule=obj_rule)
154 10 Abstract Models and Their Solution
Aside from supporting greater flexibility for the user, this option allows users to
define multiple models in a Pyomo model file and then select the model to be opti-
mized when the solve subcommand is executed.
Section 10.3.4 introduces the namespace command in Pyomo data files. This
command is used to define blocks of data commands that are integrated option-
ally into a model. The solve subcommand provides the --namespace option to
specify one or more namespaces used to construct an instance of an abstract model;
the --ns is a shorter alias for this option. For example, the command
pyomo solve --solver=glpk --namespace=data1 abstract5.py \
abstract5-ns1.dat
creates and optimizes the abstract model in abstract5.py using the following
data commands:
namespace data1 {
set N := 1 2 ;
set M := 1 2 ;
param c :=
1 1
2 2 ;
param a :=
1 1 3
2 1 4
1 2 2
2 2 5 ;
param b :=
1 1
2 2 ;
}
namespace data2 {
set N := 3 4 ;
set M := 5 6 ;
10.2 The pyomo Command 155
param c :=
3 10
4 20 ;
param a :=
3 5 3
4 5 4
3 6 2
4 6 5 ;
param b :=
5 1
6 2 ;
}
This command specifies the data1 namespace, which has an optimal solution of
0.8. Similarly, the command
pyomo solve --solver=glpk --namespace=data2 abstract5.py \
abstract5-ns1.dat
creates and optimizes the same model using the data2 namespace, which has an
optimal solution of 8. A different index set is used in the data2 data, as well as
different objective coefficients.
The previous example illustrates how namespaces allow the user to specify dif-
ferent data sets within a single data command file. Note that a model can be con-
structed from data commands using multiple namespaces, including data not in a
namespace. Consider the following data commands:
set N := 1 2;
namespace c1 {
param c :=
1 1
2 2 ;
}
namespace c2 {
param c :=
1 10
2 20 ;
}
namespace data1 {
set M := 1 2 ;
param a :=
1 1 3
2 1 4
1 2 2
2 2 5 ;
param b :=
156 10 Abstract Models and Their Solution
1 1
2 2 ;
}
namespace data2 {
set M := 5 6 ;
param a :=
1 5 3
2 5 4
1 6 2
2 6 5 ;
param b :=
5 1
6 2 ;
}
This includes four namespaces and data commands outside of a namespace. The
command
pyomo solve --solver=glpk --namespace=c1 --namespace=data2 \
abstract5.py abstract5-ns2.dat
creates and optimizes the abstract modeling in abstract5.py using data com-
mands from the c1 and data2 namespaces, as well as the data command for N,
which is outside of any namespace. Note that if multiple namespaces contain data
commands for the same component, then the component is initialized with the data
from first namespace containing the corresponding data command. If there is not
a namespace containing a corresponding data command, then the data commands
outside of namespaces are used to initialize the component.
The different steps that are executed by the solve subcommand represent a generic
workflow for model construction and optimization. This workflow can be cus-
tomized using a variety of callback functions that are defined within a Pyomo model
file. These callback functions allow the user to define additional analysis steps, as
well as replace some of the default steps in the workflow.
10.2 The pyomo Command 157
Function Description
pyomo preprocess Perform a preprocessing step before model construction
pyomo create model Construct and return a model object
pyomo create modeldata Construct and return a DataPortal object
pyomo print model Output model object information
pyomo modify instance Modify the model instance
pyomo print instance Output model instance information
pyomo save instance Save the model instance
pyomo print results Print the optimization results
pyomo save results Save the optimization results
pyomo postprocess Perform a postprocessing step after optimization
Table 10.2: Callback functions that can be used in a Pyomo model file to customize the workflow
in the pyomo solve subcommand.
Table 10.2 summarizes the pyomo command callback functions and the func-
tionality that they support. Each callback function takes one or more keyword argu-
ments in the form keyword=value. For example, the pyomo print results
callback function takes three arguments: options, instance, and results.
def pyomo_print_results(options=None, instance=None,
results=None):
print(results)
There are several standard arguments for the callback functions described in
Table 10.2. The options argument is an enhanced Python dictionary contain-
ing the command-line options sent to the solve subcommand. The model ar-
gument is the Pyomo model object, and the instance argument is the model
instance constructed from this model. In the case where the user defines a model
using ConcreteModel, then the model and instance arguments are the same
object. Other arguments are described with their associated callback functions.
pyomo preprocess
This callback function is used to construct a model. This function has two argu-
ments: options and model options. The latter argument contains the options
for constructing the model, which are specified with the --model-options
command-line option. The return value of this function must be the model object
created, which may be either an abstract or concrete model. For example, the fol-
lowing callback function creates a model by importing the abstract6.py file
and then returning the Model object:
def pyomo_create_model(options=None, model_options=None):
sys.path.append(abspath(dirname(__file__)))
abstract6 = __import__(’abstract6’)
sys.path.remove(abspath(dirname(__file__)))
return abstract6.Model
This callback function creates a model data object used to create a model instance.
Model data objects are useful in contexts where a set of different data sources
need to be specified for model constructions. This function has two arguments:
options and model. The return value must be a DataPortal object. For ex-
ample, the following callback function creates a DataPortal object from the file
abstract6.dat:
def pyomo_create_dataportal(options=None, model=None):
data = pyo.DataPortal(model=model)
data.load(filename=’abstract6.dat’)
return data
This callback function prints an abstract model before a model instance is created.
This function has two arguments: options and model. The following example
calls the pprint method to print detailed information about an abstract model:
def pyomo_print_model(options=None, model=None):
if options[’runtime’][’logging’]:
model.pprint()
This callback function modifies the model instance after it has been constructed.
This function has three arguments: options, model, and instance. The fol-
10.2 The pyomo Command 159
This callback function prints the Pyomo model instance. This function is used to
print the concrete model instance rather than the abstract model. This function
has two arguments: options and instance. The following example calls the
pprint method to print detailed information about a model instance:
def pyomo_print_instance(options=None, instance=None):
if options[’runtime’][’logging’]:
instance.pprint()
This callback function saves the Pyomo model instance. This function has two ar-
guments: options and instance. Note that Pyomo does not specify how the
model is saved. However, a convenient mechanism would be to use Python’s pickle
mechanism:
def pyomo_save_instance(options=None, instance=None):
OUTPUT = open(’abstract7.pyomo’,’w’)
OUTPUT.write(str(pickle.dumps(instance)))
OUTPUT.close()
This callback function prints the results generated from optimization. This function
has three arguments: options, instance, and results. The results object
supports a generic summary of optimization solutions, solver statistics, etc. in both
the JSON or YAML formats. Thus, this callback function can simply print this data:
def pyomo_print_results(options=None, instance=None,
results=None):
print(results)
This callback function is used to save the results generated from optimization. This
function has three arguments: options, instance, and results. This call-
back function can simply print the results to a file:
def pyomo_save_results(options=None, instance=None,
results=None):
OUTPUT = open(’abstract7.results’,’w’)
OUTPUT.write(str(results))
OUTPUT.close()
pyomo postprocess
The generic workflow supported by the solve subcommand includes the execution
of a solver to optimize (or otherwise analyze) a model. A variety of command-line
options are used to control solver behavior. The --solver option is used to specify
the name of the solver constructed. This option can specify two classes of solvers:
the names of command-line executables on the user’s path, and predefined solver in-
terfaces.
Command-line executables are assumed to perform I/O using NL files. Thus,
command-line executables can be optimized with any solver executable built with
the AMPL solver library.
Solver options can be specified in a generic manner using the
--solver-options option. This specifies a string interpreted as one or more
option-value pairs. For example, the following option passes the mipgap option to
the glpk solver:
pyomo solve --solver=glpk --solver-options=’mipgap=0.01’ \
concrete1.py
10.2 The pyomo Command 161
Additionally, the --timelimit option can be used to specify the maximum run-
time of the solver. This is typically passed to the solver, and thus this timelimit is
enforced in a solver-dependent manner.
Solver results are generated from solution information provided by the solver,
and optionally a logfile of output from the solver. By default, Pyomo captures infor-
mation about the variable values selected by the solver. However, there is often addi-
tional information a user may wish to collect, such as dual values for constraints in a
linear program. For performance reasons, this data is not automatically collected by
the solve subcommand, but the --solver-suffixes option is used to specify
the names of the data desired. A suffix is simply data for a constraint or variable that
results from the application of a solver. Suffixes can be specified by name, or with a
regular expression. For example, the following command specifies that all suffixes
generated by the solver are requested:
pyomo solve --solver=glpk --solver-suffix=’.*’ concrete2.py
Note that a given solver may provide only a subset of these suffixes.
The --tempdir and --keepfiles options can be used to archive the tem-
porary files that Pyomo uses. By default, Pyomo uses temporary files automatically
generated in system temporary directories. The --tempdir option is used to spec-
ify the directory that these files are created in. By default, temporary files are deleted
after optimization is completed. The --keepfiles options disables this deletion,
which allows the user to see the data Pyomo sends to the optimizer.
The --postprocess option can be used to specify a Python module that is ex-
ecuted after the solver has executed. A typical use of this option is to specify post-
processing steps to interpret the solver results in a problem-dependent manner.
Post-processing steps can be defined by declaring in the Python modules a
pyomo postprocess function to be used in post processing. Figure 10.2 pro-
vides an example of a post-processing function that writes the final solutions to a
file in the CSV format.
import csv
Fig. 10.2: A post-processing plugin that writes final solutions in a CSV file.
The default output of the solve subcommand is a terse summary of the ma-
jor steps that are executed. The --log and --stream-output options are
used to print the solver output. The --log option is used to print the solver
output after the solver has terminated, and the --stream-output option is
used to print the solver output as it is generated. Similarly, the --summary and
--show-results options print different summaries of the optimization results.
The --summary command prints a summary of the Pyomo model, after the results
are loaded.
The --show-results prints the final results. If the PyYAML package is in-
10.2 The pyomo Command 163
stalled, then the default results format is YAML and the final results are stored in
the file results.yml. Otherwise, the default results format is JSON and the fi-
nal results are stored in the file results.json. The --json option can be used
to specify the JSON results format when the PyYAML package is installed. The
--save-results option can be used to specify an alternative results file.
Pyomo uses a standard Python logging system to manage the printing of log-
ging messages for the underlying software in Pyomo. By default, logging messages
representing Pyomo errors and warnings are always printed. The --quiet option
suppresses all log messages except for those referring to errors. The --warning
option enables warning messages for Pyomo. The --info option enables informa-
tive, warning and error log messages for Pyomo.
The --verbose option enables debugging log messages for Pyomo. This op-
tion can be specified multiple times to enable logging messages for different parts
of Pyomo: (1) debugging for just Pyomo and (2) debugging for all Pyomo pack-
ages. The --debug option enables debugging logging, and it allows exceptions to
trigger a failure in which the program stack is printed.
The --output option can also be used to specify a filename, for which the
filename suffix specifies the file format. For example, the command
pyomo convert --output=concrete1.lp concrete1.py
The Set and Param components of a Pyomo model are used to define data values
used to construct constraints and objectives. Previous chapters have illustrated that
these components are not necessary to develop complex models. However, the Set
and Param components can be used to define abstract data declarations, where no
data values are specified. For example:
model.A = Set(within=Reals)
model.p = Param(model.A, within=Integers)
Data command files can be used to initialize data declarations in Pyomo models,
and in particular they are useful for initializing AbstractModel data declara-
tions. However, note that complex mappings are often accomplished in Pyomo via
scripting rather than using data command files.
Pyomo’s data command files employ a domain-specific language whose syntax
closely resembles the syntax of AMPL’s data commands [2]. A data command file
consists of a sequence of commands specifing set and parameter data, or specifing
where such data is to be obtained from external sources. The following commands
can be used to declare data:
• The set command declares set data.
• The param command declares a table of parameter data, which can also include
the declaration of the set data used to index the parameter data.
• The load command loads data from an external resources, like a spreadsheet
or database.
• The table command declares a two-dimensional table of parameter data.
The following commands can also be used in data command files:
• The include command specifies a data command file that is processed im-
mediately.
• The data and end commands do not perform any actions, but they provide
compatibility with AMPL scripts that define data commands.
Finally, the namespace declaration allows data commands to be organized into
named groups allowing each to be enabled or disabled during model construction.
10.3 Data Commands for AbstractModel 165
Note that Pyomo’s data commands do not exactly correspond to AMPL data
commands. The set and param commands are designed to closely match AMPL’s
syntax and semantics. However, these commands only support a subset of the corre-
sponding declarations in AMPL. However, it is not possible to support other AMPL
commands because Pyomo treats data commands as data declarations while AMPL
treats data commands as part of its scripting language.
The following subsections describe the syntax Pyomo’s data file commands ex-
cept for the load and table commands, which are documented via Pyomo’s
online documentation. The syntax of data commands can be quite varied, and we
provide detailed examples to illustrate these commands. Note that all Pyomo data
commands are terminated with a semicolon, and the syntax of data commands does
not depend on whitespace. Thus, data commands can be broken across multiple lines
– newlines and tab characters are ignored – and data commands can be formatted
with whitespace with few restrictions.
The set data command explicitly specifies the members of either a single set or an
array of sets, i.e., an indexed set. A single set is specified with a list of data values
that are included in this set. The formal syntax for the set data command is:
set <setname> := [<value>] ... ;
The data values in a set consist of either numeric values, simple strings or quoted
strings:
• Numeric values are any string that can be evaluated by Python as a numeric
value, e.g., integer, float, scientific notation, or boolean.
• Simple strings are sequences of alpha-numeric characters.
• Quoted strings are simple strings that are included in a pair of single or double
quotes. A quoted string can include quotes within the quoted string.
There is no restriction on the values in a set declaration. A set may be empty, and it
may contain any combination of numeric and non-numeric string values. Validation
of set data is performed when constructing a Pyomo model, not while parsing a data
command file. For example, the following are valid set commands:
# An empty set
set A := ;
# A set of numbers
set A := 1 2 3;
# A set of strings
set B := north south east west;
166 10 Abstract Models and Their Solution
Note that numeric values are automatically converted to Python integer or floating
point values when the set data specification is parsed. A quoted string can be used
to define a string value containing a numeric value. However, if the string strictly
specifies a numeric value, it will be converted by Python to a numeric type. For ex-
ample, the string “100” is included in set C, but this value is converted to a numeric
value.
The set data command can also specify tuple data with the standard notation for
tuples. For example, suppose set A contains 3-tuples:
model.A = pyo.Set(dimen=3)
The following set data command then specifies that A is the set containing the
tuples (1,2,3) and (4,5,6):
set A := (1,2,3) (4,5,6) ;
Alternatively, set data can simply be listed in the order that the tuple is represented:
set A := 1 2 3 4 5 6 ;
Obviously, the number of data elements specified using this syntax should be a
multiple of the set dimension.
Sets with 2-tuple data can also be specified in a matrix denoting set membership.
For example, the following set data command declares 2-tuples in A using + to
denote valid tuples and - to denote invalid tuples:
set A : A1 A2 A3 A4 :=
1 + - - +
2 + - + -
3 - + - - ;
This data command declares the following five 2-tuples: (’A1’,1), (’A1’,2), (’A2’,3),
(’A3’,2), (’A4’,1).
Finally, a set of tuple data can be concisely represented with tuple templates that
represent a slice of tuple data. For example, suppose the set A contains 4-tuples:
model.A = pyo.Set(dimen=4)
The following set data command declares groups of tuples defined by a template
and data to complete this template:
10.3 Data Commands for AbstractModel 167
set A :=
(1,2,*,4) A B
(*,2,*,4) A B C D ;
The set data command can also be used to declare data for a set array. Each set in
a set array must be declared with a separate set data command with the following
syntax:
set <set-name>[<index>] := [<value>] ... ;
Set arrays can be indexed by an arbitrary set and the index value may be a numeric
value, a non-numeric string value, or a comma-separated list of string values.
Suppose set A is used to index a set B as follows:
model.A = pyo.Set()
model.B = pyo.Set(model.A)
set B[1] := 0 1 2;
set B[aaa] := aa bb cc;
set B[’a b’] := ’aa bb cc’;
Parameters can be defined with numeric and string data. Numeric data is defined
with a string evaluated by Python as a numeric value, which includes integer, float-
ing point, scientific notation, and boolean. Boolean values can be specified with a
variety of strings: TRUE, true, True, FALSE, false, and False. Note that pa-
rameters cannot be defined without data, so there is no analog to the specification of
an empty set.
Most parameter data is indexed over one or more sets, and there are a number of
ways the param data command can be used to specify indexed parameter data.
A param data command can specify values for B with a list of index-value pairs:
set A := a c e;
param B := a 10 c 30 e 50;
Because whitespace is ignored, this example data command file can be reorganized
to specify the same data in a tabular format:
set A := a c e;
param B :=
a 10
c 30
e 50
;
Multiple parameters can be defined using a single param data command. For ex-
ample, suppose parameters B, C, and D are one-dimensional parameters all indexed
by the set A:
model.A = pyo.Set()
model.B = pyo.Param(model.A)
model.C = pyo.Param(model.A)
model.D = pyo.Param(model.A)
Values for these parameters can be specified using a single param data command
declaring these parameter names followed by a list of index and parameter values:
set A := a c e;
param : B C D :=
a 10 -1 1.1
c 30 -3 3.3
e 50 -5 5.5
;
10.3 Data Commands for AbstractModel 169
The values in the param data command are interpreted as a list of sublists, where
each sublist consists of an index followed by the corresponding numeric value.
Note that parameter values do not need to be defined for all indices. For example,
the following data command file is valid:
set A := a c e g;
param : B C D :=
a 10 -1 1.1
c 30 -3 3.3
e 50 -5 5.5
;
The index g is omitted from the param command, and consequently this index is
not valid for the model instance using this data. More complex patterns of missing
data can be specified using the “.” character to indicate a missing value. This syntax
is useful when specifying multiple parameters that do not necessarily have the same
index values:
set A := a c e;
param : B C D :=
a . -1 1.1
c 30 . 3.3
e 50 -5 .
;
Finally, we note that default values for missing data can also be specified using
the default keyword:
set A := a c e;
Note that default values can only be specified in param commands defining values
for a single parameter.
170 10 Abstract Models and Their Solution
The syntax of the param data command remains essentially the same when speci-
fying values for B with a list of index and parameter values:
set A := a 1 c 2 e 3;
param B :=
a 1 10
c 2 30
e 3 50;
Missing and default values are also handled in the same way with multi-dimensional
index sets:
set A := a 1 c 2 e 3;
param B default 0 :=
a 1 10
c 2 .
e 3 50;
Similarly, multiple parameters can defined with a single param data command.
Suppose that parameters B, C, and D are parameters indexed over set A with dimen-
sion 2:
model.A = pyo.Set(dimen=2)
model.B = pyo.Param(model.A)
model.C = pyo.Param(model.A)
model.D = pyo.Param(model.A)
These parameters can be defined with a single param command that declares the
parameter names followed by a list of index and parameter values:
set A := a 1 c 2 e 3;
param : B C D :=
a 1 10 -1 1.1
c 2 30 -3 3.3
e 3 50 -5 5.5
;
Similarly, the following param data command defines the index set along with the
parameters:
param : A : B C D :=
a 1 10 -1 1.1
c 2 30 -3 3.3
e 3 50 -5 5.5
;
10.3 Data Commands for AbstractModel 171
The param command also supports a matrix syntax for specifying the values in
a parameter with a 2-dimensional index. Suppose parameter B is indexed over set A
with dimension 2:
model.A = pyo.Set(dimen=2)
model.B = pyo.Param(model.A)
param B : a c e :=
1 1 2 3
2 4 5 6
3 7 8 9
;
param B (tr) : 1 2 3 :=
a 1 4 7
c 2 5 8
e 3 6 9
;
The following param command defines a matrix of parameter values with mul-
tiple templates:
set A := (a,1,a,1) (a,2,a,2) (b,1,b,1) (b,2,b,2);
param B :=
[*,1,*,1] a a 10 b b 20
[*,2,*,2] a a 30 b b 40
;
The include command allows a data command file to execute data commands
from another file. For example, the following command file executes data commands
from ex1.dat and then ex2.dat:
include ex1.dat;
include ex2.dat;
Pyomo is sensitive to the order of execution of data commands, since data com-
mands can redefine set and parameter values. The include command respects
this data ordering; all data commands in the included file are executed before the
remaining data commands in the current file are executed.
The namespace keyword is not a data command, but instead it is used to structure
the specification of Pyomo’s data commands. Specifically, a namespace declaration
is used to group data commands and to provide a group label. Consider the following
data command file:
set C := 1 2 3 ;
namespace ns1
{
set C := 4 5 6 ;
}
namespace ns2
{
set C := 7 8 9 ;
}
This data file defines two namespaces: ns1 and ns2 that initialize a set C. By
default, data commands contained within a namespace are ignored during model
construction; when no namespaces are specified, the set C has values 1,2,3. When
namespace ns1 is specified, then set C values are overridden with the set 4,5,6.
See Section 10.2.2.2 for an example of how namespaces are selected with the
pyomo command.
model = pyo.AbstractModel()
def checkPN_rule(model):
return model.P <= len(model.N)
model.checkPN = pyo.BuildCheck(rule=checkPN_rule)
def obj_rule(model):
return sum(model.d[n,m]*model.x[n,m] for n in model.N for m \
in model.M)
model.obj = pyo.Objective(rule=obj_rule)
def num_warehouses_rule(model):
return sum(model.y[n] for n in model.N) <= model.P
model.num_warehouses = pyo.Constraint(rule=num_warehouses_rule)
174 10 Abstract Models and Their Solution
def printM_rule(model):
model.M.pprint()
model.printM = pyo.BuildAction(rule=printM_rule)
param d :=
Harlingen NYC 1956
Harlingen LA 1606
Harlingen Chicago 1410
Harlingen Houston 330
Memphis NYC 1096
Memphis LA 1792
Memphis Chicago 531
Memphis Houston 567
Ashland NYC 485
Ashland LA 2322
Ashland Chicago 324
Ashland Houston 1236
;
param P := 4 ;
Abstract This chapter documents how to express and solve Generalized Disjunctive
Programs (GDPs). GDP models provide a structured approach for describing logical
relationships in optimization models. We show how Pyomo blocks provide a natural
base for representing disjuncts and forming disjunctions, and we how to solve GDP
models through the use of automated problem transformations.
11.1 Introduction
Ly ≤ x
x ≤ Uy
y ∈ {0, 1}
When U is infinite, then the second inequality is replaced with a so-called “Big-M”
constraint:
x ≤ My
for a value M chosen to be sufficiently big so as to not limit the space of feasible
solutions (although it does imply a finite U). One can readily see that when y = 1
the inequalities enforce that x lie in the continuous range L ≤ x ≤ U, and when y = 0
the inequalities reduce to 0 ≤ x ≤ 0, or x = 0.
177
178 11 Generalized Disjunctive Programming
In this formulation, the binary variable y is not intrinsic to the constraints, but
rather captures a logical condition: indicating whether one set of mututally exclusive
constraints (L ≤ x ≤ U) or another (x = 0) must be enforced. In this way, the variable
is often referred to as an indicator variable. Indeed, it can be argued that the bulk
of binary variables used in math programming are in fact indicator variables used to
capture logic in the model.
This approach to formulating a switching decision can be generalized to switch
between different groups of constraints. For example, consider the Unit Commit-
ment Problem, where the goal of the optimization problem is to determine the min-
imal cost schedule for turning on and off a fleet of generators in order to meet
expected demand. In this case, a generator can exist in one of several states: on, off,
starting up, and shutting down. Selecting a particular state implies additional con-
straints on the generator operation. If a generator is “on”, then the output power is
bounded between the minimum (nonzero) and maximum power levels. In contrast,
when the generator is “off”, the output power must be 0. Further, the output power
in any time period must be within “ramp limits” of output power in the previous
time period.
One implementation of these state selection rules expresses all the constraints
and relaxes them based on binary state (indicator) variables using so-called “Big-
M” terms:
of Disjunctive Programming [6] for integer linear problems to also include nonlin-
ear systems. The canonical GDP model [38] augments the objective, variables, and
constraints of a typical MI(N)LP with Boolean variables, disjunctions, and logical
constraints:
This modeling approach directly addresses the two limitations of typical MI(N)LP
models discussed previously: the relationship between the switching variable and
the constraints it implies is now explicit in the model structure, and the model is no
longer locked into any particular relaxation.
180 11 Generalized Disjunctive Programming
A disjunct is logically a container for the indicator variable and the corresponding
constraints. Here we see the power of the hierarchical modeling approach enabled
by the Block component: the Disjunct component is naturally derived from the
Block class. As with blocks, Disjunct components may be arbitrarily indexed
and initialized through rules. In addition, they may contain any Pyomo modeling
component, including not only Sets, Params, Vars, and Constraints, but also
Blocks, Disjuncts, and Disjunctions. The only thing that the Disjunct
class adds to the normal Block implementation is the implicit and automatic defi-
nition of the disjunct’s Boolean indicator variable.
For our generator state example, the requisite three disjuncts are declared as fol-
lows:
11.2 Modeling GDP in Pyomo 181
model.NumTimePeriods = pyo.Param()
model.GENERATORS = pyo.Set()
model.TIME = pyo.RangeSet(model.NumTimePeriods)
model.MaxPower = pyo.Param(model.GENERATORS, \
within=pyo.NonNegativeReals)
model.MinPower = pyo.Param(model.GENERATORS, \
within=pyo.NonNegativeReals)
model.RampUpLimit = pyo.Param(model.GENERATORS, \
within=pyo.NonNegativeReals)
model.RampDownLimit = pyo.Param(model.GENERATORS, \
within=pyo.NonNegativeReals)
model.StartUpRampLimit = pyo.Param(model.GENERATORS, \
within=pyo.NonNegativeReals)
model.ShutDownRampLimit = pyo.Param(model.GENERATORS, \
within=pyo.NonNegativeReals)
def Power_bound(m,g,t):
return (0, m.MaxPower[g])
model.Power = pyo.Var(model.GENERATORS, model.TIME, \
bounds=Power_bound)
Note that while the disjuncts may be completely self-contained, with their own local
variables, parameters, and constraints, they may also reference Pyomo components
outside their immediate scope.
The Disjunction component is used to associate a set of disjuncts. A dis-
junction is similar to a constraint, in that it can be indexed and defined through
rules. However, unlike a Constraint, where the rule returns a relational expres-
sion, the rule for a Disjunction must return a list of Disjuncts. Subsequent
model transformations will convert the Disjunction component and generate
the binding constraint across the disjuncts. While the general form of a GDP relates
the disjuncts using an “OR” operator, the vast majority of models actually expect an
“exactly one” relationship (a generalization of the “exclusive OR” operator). This is
so common that the default behavior of the Disjunction component is to gen-
erate the “exactly one” relationship. Modelers may, however, specify the original
“OR” operator by providing xor=False to the Disjunction declaration.
The disjunction for our generator state example is properly an “exactly one”
relationship, and can be expressed in Pyomo using:
def bind_generators(m, g, t):
return [m.GenOn[g, t], m.GenOff[g, t], m.GenStartup[g, t]]
model.bind_generators = Disjunction(
model.GENERATORS, model.TIME, rule=bind_generators)
While Disjunct blocks capture the indicator relationship between the Boolean
indicator var and the constraints contained on a disjunct, we also wish to ex-
press additional logical constraints among the disjunct indicator variables. To sup-
port this, Pyomo provides a LogicalConstraint component for representing
logical constraints in a model. As is the case with Constraint components,
LogicalConstraint components may be single or indexed, and are initial-
ized using an explit expr keyword argument, or passed a rule function through
the rule keyword. Where they differ is in the type of expression that they take:
whereas Constraint accepts relational expressions that are equality or inequal-
ity expressions, LogicalConstraint accepts a logical expression.
Logical epressions are built using an expression system with Boolean variables
and Boolean constants combined using logical operators. Table 11.1 shows the oper-
ators supported for Logical Expressions. The operations for logical expressions in-
tentionally do not overlap with the operations supported for algebraic expressions,
thereby enforcing a semantic distinction between binary (algebraic) variables and
Boolean (logical) variables.
11.4 Solving GDP models 183
Table 11.1: Supported operations for generating logical expressions. In these examples, X, Y, and
Z are declared BooleanVar variables (the model. is omitted due to space limitations).
For our generator state example from the previous section, we will use logical
constraints to capture the state transition rules describing how the generator state
can change between two time periods. We can express the switching rules as:
def onState(m, g, t):
if t == m.TIME.first():
return pyo.LogicalConstraint.Skip
return m.GenOn[g, t].indicator_var.implies(pyo.lor(
m.GenOn[g, t-1].indicator_var,
m.GenStartup[g, t-1].indicator_var))
model.onState = pyo.LogicalConstraint(
model.GENERATORS, model.TIME, rule=onState)
While special-purpose solvers are being developed in Pyomo able to parse and ma-
nipulate generalized disjunctive programming models, Pyomo’s standard solver in-
terfaces cannot express directly either disjunctions or logical constraints. However,
Pyomo includes the capability to transform a disjunctive model into an equivalent
184 11 Generalized Disjunctive Programming
MI(N)LP model by converting the logical constraints into their equilvaent linear
forms and relaxing the disjunctive constraints. The transformed (relaxed) model
can then be solved by an appropriate solver through the standard solver interfaces.
Pyomo’s GDP package provides two automated relaxations: the first relaxes the
disjunctive constraints by adding so-called “Big-M” terms (recovering the original
model structure from Section 11.1) and the second explicitly generates the hull re-
laxation of the individual disjunctions.
and
∑ bk ≥ 1 (11.22)
k∈K
where bk is the binary variable associated with the Boolean variable dk .indicator var.
The transformation name gdp.bigm is used to apply the Big-M transformation.
11.5 A mixing problem with semi-continuous variables 185
The hull transformation relaxes the original disjunctive model by generating a lifted
representation. The transformation follows the procedure of Balas [6] for linear dis-
junctions and Lee and Grossmann [38] (with modifications from Sawaya and Gross-
mann [55]) for nonlinear disjunctions. In both cases, the variables appearing in each
disjunct are “disaggregated” by defining new variables for each disjunct and con-
straining the original variable to be the sum of the disaggregated variables. This has
the effect of representing the disjunction as the affine combination of the individual
disjuncts. For disjunctions with only convex constraints, the affine combination of
the disaggregated (lifted) disjuncts defines the convex hull of the disjuncts. The log-
ical Disjunction relationship is converted to algebraic form using the same approach
as Big-M (Equations 11.19-11.22).
By constraining the disaggregated variables to be zero when the disjunct’s indica-
tor variable is False, the solution to the discrete relaxed problem will be the solution
to the original disjunctive problem. This increases the overall size of the model (both
the number of variables and constraints), but gives a tighter continuous relaxation
than the Big-M transformation. However, all variables must be bounded to apply the
hull disjunction, and the Disjunctions can only express exclusive relationships (i.e.,
xor=True)
The transformation name gdp.hull is used to apply the Hull transformation.
The following model illustrates a simple mixing problem with three semi-continuous
variables (x1 , x2 , x3 ) which represent quantities that are mixed to meet a volumetric
constraint. In this simple example, the number of sources is minimized:
# scont.py
import pyomo.environ as pyo
from pyomo.gdp import Disjunct, Disjunction
L = [1,2,3]
U = [2,4,6]
index = [0,1,2]
model = pyo.ConcreteModel()
model.x = pyo.Var(index, within=pyo.Reals, bounds=(0,20))
model.x_nonzero = pyo.Var(index, bounds=(0,1))
There are three ways to apply either the Big-M or Hull transformation to solve
this model:
1. through the pyomo command line,
2. through a scripting interface, or
3. through a BuildAction.
On the pyomo command line, the --transform command line option is used to
apply a transformation:
pyomo solve scont.py --transform gdp.bigm --solver=glpk
The equivalent approach when developing custom scripts is to create the trans-
formation before applying it to the model:
xfrm = pyo.TransformationFactory(’gdp.bigm’)
xfrm.apply_to(model)
solver = pyo.SolverFactory(’glpk’)
status = solver.solve(model)
Finally, there are situations where you will want to inject transformations into
models that are generated and manipulated in environments other than the pyomo
command or custom scripts (e.g., the runph script). In this case, you can trigger
the transformation by adding a BuildAction to the model:
def transform_gdp(m):
xfrm = pyo.TransformationFactory(’gdp.bigm’)
xfrm.apply_to(m)
model.transform_gdp = pyo.BuildAction(rule=transform_gdp)
Chapter 12
Differential Algebraic Equations
Abstract This chapter documents how to formulate and solve optimization prob-
lems with differential and algebraic equations (DAEs). The pyomo.dae package
allows users to incorporate detailed dynamic models within an optimization frame-
work, and it is flexible enough to represent a wide variety of differential equations.
pyomo.dae also includes several automated solution techniques based on a simul-
taneous discretization approach to solve dynamic optimization problems.
12.1 Introduction
min x3 (t f ) (12.1)
s.t. ẋ1 = x2 (12.2)
ẋ2 = −x2 + u (12.3)
ẋ3 = x12 + x22 + 0.005 · u2 (12.4)
x2 − 8 · (t − 0.5)2 + 0.5 ≤ 0 (12.5)
x1 (0) = 0, x2 (0) = −1, x3 (0) = 0,t f = 1 (12.6)
where the objective is to minimize the value of x3 at the final time point by finding
the optimal values for the input variable u. This problem includes three differential
187
188 12 Differential Algebraic Equations
The pyomo.dae package defines two new components used to represent DAE
models in Pyomo:
• ContinuousSet represents continuous domains over which a derivative can
be taken, and
• DerivativeVar represents the derivative of a Var with respect to a given
ContinuousSet.
The package is explicitly imported to access these modeling components:
import pyomo.environ as pyo
import pyomo.dae as dae
m.tf = pyo.Param(initialize=1)
m.t = dae.ContinuousSet(bounds=(0,m.tf))
m.x1dotcon[m.t.first()].deactivate()
m.x2dotcon[m.t.first()].deactivate()
m.x3dotcon[m.t.first()].deactivate()
The last important aspect of any dynamic optimization problem is the specifi-
cation of initial or boundary conditions. This can be achieved by fixing a Var or
DerivativeVar at one of the bounds of a ContinuousSet. The bounds of a
ContinuousSet can be accessed using the first() or last() accessor meth-
ods on the ContinuousSet. For example, the initial conditions for the optimal
control example can be implemented as:
m.x1[0].fix(0)
m.x2[m.t.first()].fix(-1)
m.x3[m.t.first()].fix(0)
After applying the backward difference method to the continuous domain t, the
resulting derivative and constraint pair is
dx xk+1 − xk
= , k = 0, ..., N − 1 (12.8)
dt tk+1 h
!
dx
g , f (xk+1 , uk+1 ) = 0, k = 0, ..., N − 1 (12.9)
dt tk+1
where xk = x(tk ), tk = kh, and h is the step size between discretization points or
the size of each finite element. When a finite difference transformation is applied
to a Pyomo model, the discretization equations such as (12.8) are automatically
generated and added to the Pyomo model as equality constraints.
The code required to apply the backward finite difference method to our optimal
control example is as follows:
discretizer = pyo.TransformationFactory(’dae.finite_difference’)
discretizer.apply_to(m, nfe=20, wrt=m.t, scheme=’BACKWARD’)
The nfe keyword argument stands for “number of finite elements”, and it spec-
ifies the number of discretization points that are used in the discretization. The
scheme keyword specifies which finite difference method to apply. There currently
are three finite difference schemes included in pyomo.dae: backward finite differ-
ence (’BACKWARD’), central finite difference (’CENTRAL’), and forward finite
difference (’FORWARD’).
192 12 Differential Algebraic Equations
K
dx 1 d` j (τk )
= ∑ xi j , k = 1, . . . , K, i = 1, . . . , N − 1 (12.10)
dt ti j hi j=0 dτ
!
dx
0=g , f (xik , uik ) , k = 1, . . . , K, i = 1, . . . , N − 1 (12.11)
dt ti j
K
xi+1,0 = ∑ ` j (1)xi j , i = 1, . . . , N − 1 (12.12)
j=0
where ti j = ti−1 + τ j hi , x(ti j ) = xi j . Further, we note that the solution x(t) is interpo-
lated as follows:
K
x(t) = ∑ ` j (τ)xi j , t ∈ [ti−1 ,ti ], τ ∈ [0, 1] (12.13)
j=0
K
(τ − τk )
` j (τ) = ∏ . (12.14)
k=0,6= j (τ j − τk )
The nfe keyword argument specifies the number of finite elements and the ncp
argument specifies the number of collocation points within each finite element.
One of the main design considerations for the pyomo.dae package was the exten-
sibility of the package to include general implementations of common operations
applied to dynamic optimization problems. One such common operation in the area
of optimal control is restricting the control input to have a certain profile, typically
piecewise constant or piecewise linear. Often times when a model is discretized us-
ing collocation over finite elements the control variable is restricted to be constant
over each finite element. The pyomo.dae package includes a function for doing
this after a collocation discretization has been applied to a model. It works by re-
ducing the number of free collocation points for a particular variable. For example,
to restrict our control input u to be piecewise constant in our small optimal con-
trol problem you would add the following line right after applying a discretization
transformation:
discretizer.reduce_collocation_points(m, var=m.u, ncp=1, \
contset=m.t)
The ncp keyword argument specifies the number of free collocation points per finite
element for the variable specified by the keyword var. Specifying ncp=1 restricts
u to have a single free collocation point (or degree of freedom) rendering it constant
over each finite element. The function works by adding constraints to the discretized
model which force any extra, undesired collocation points to be interpolated from
the others.
12.4.3 Plotting
solver=pyo.SolverFactory(’ipopt’)
solver.solve(m, tee=True)
Finally, the code below shows an example implementation of a plotter function using
matplotlib for plotting. The resulting figure is also shown below.
def plotter(subplot, x, *y, **kwds):
plt.subplot(subplot)
for i,_y in enumerate(y):
plt.plot(list(x), [value(_y[t]) for t in x], ’brgcmk’[i%6])
if kwds.get(’points’, False):
plt.plot(list(x), [value(_y[t]) for t in x], ’o’)
plt.title(kwds.get(’title’,’’))
plt.legend(tuple(_y.name for _y in y))
plt.xlabel(x.name)
Fig. 12.1: Plot produced by matplotlib for the optimal control example
Chapter 13
Mathematical Programs with Equilibrium
Constraints
13.1 Introduction
197
198 13 Mathematical Programs with Equilibrium Constraints
Ferris et al. [16] note that there are a few fundamental forms accounting for a wide
range of complementarity conditions that arise in practice. Consider a variable x and
function g(x). The classical form of complementarity condition can be expressed as
x ≥ 0 ⊥ g(x) ≥ 0,
which expresses the complementarity restriction that at least one of these must hold
with equality. When the variable x is bounded such that x ∈ [l, u], then a mixed
complementarity condition can be expressed as
l ≤ x ≤ u ⊥ g(x),
which expresses the complementarity restriction that at least one of the following
must hold:
x=l and g(x) ≥ 0,
x=u and g(x) ≤ 0,
or l < x < u and g(x) = 0.
These forms can be generalized by substituting a function f (x) for the variable
x. Thus, a generalized mixed complementarity condition can be expressed as
l ≤ f (x) ≤ u ⊥ g(x),
which expresses the complementarity restriction that at least one of the following
must hold:
f (x) = l and g(x) ≥ 0,
f (x) = u and g(x) ≤ 0, (13.1)
or l < f (x) < u and g(x) = 0.
For completeness, note that the complementarity condition
f (x) ⊥ g(x) = 0
expr1 = expr2
expr3 ≤ expr4
const 1 ≤ expr5 ≤ const 2
where const i are constant arithmetic expressions that may only contain variables
that are fixed, and expri are arithmetic expressions containing unfixed variables.
A complementarity condition is defined with a pair of constraint expressions
l1 ≤ expr1 ≤ u1 ⊥ l2 ≤ expr2 ≤ u2 ,
where exactly two of the constant bounds l1 , u1 , l2 and u2 are finite. The non-finite
bounds values are omitted in practice, so this condition directly describes a classical
or mixed complementarity condition. Additionally, a complementarity condition can
be expressed with a simple inequality, such as:
min 2x − y
0≤y⊥y≥x
x, y ≥ 0
200 13 Mathematical Programs with Equilibrium Constraints
model = pyo.ConcreteModel()
model.compl = Complementarity(
expr=complements(0 <= model.y,
model.y >= model.x) )
The first lines in this script import Pyomo packages. The pyomo.environ pack-
ages initializes Pyomo’s environment, and pyomo.mpec defines modeling com-
ponents for complementarity conditions. The subsequent lines in this script create
a model, declare variables x and y, declare an objective f1, and declare a comple-
mentarity condition compl.
The complementarity condition is declared with the Complementarity com-
ponent. In the simplest case, this Python class takes a keyword argument expr con-
taining the value of the complements function. This function accepts two Pyomo
constraint expressions used to declare a complementarity condition.
Pyomo also supports indexed components, where a set of components are ini-
tialized over an index set using a construction rule. Thus, the Complementarity
component can be declared with an index set. For example, consider the following
model:
min ∑ni=1 i(xi − 1)2
0 ≤ xi ⊥ 0 ≤ xi+1 i = 1, . . . , n − 1
The following script defines a Pyomo implementation of the model with n = 5:
# ex1a.py
import pyomo.environ as pyo
from pyomo.mpec import Complementarity, complements
n = 5
model = pyo.ConcreteModel()
model.f = pyo.Objective(expr=sum(i*(model.x[i]-1)**2
for i in range(1,n+1)) )
component indexed over the set 1, . . . , n − 1 and initialized with a construction rule
compl . This rule is a function that accepts a model instance and an index, and
returns the i-th complementarity condition.
The declared set of indexes may be a superset of the indices defining comple-
mentarity conditions. If a construction rule returns Complementarity.Skip,
then the corresponding index is skipped. For example:
202 13 Mathematical Programs with Equilibrium Constraints
# ex1d.py
import pyomo.environ as pyo
from pyomo.mpec import Complementarity, complements
n = 5
model = pyo.ConcreteModel()
model.f = pyo.Objective(expr=sum(i*(model.x[i]-1)**2
for i in range(1,n+1)) )
n = 5
model = pyo.ConcreteModel()
model.f = pyo.Objective(expr=sum(i*(model.x[i]-1)**2
for i in range(1,n+1)) )
model.compl = ComplementarityList()
model.compl.add(complements(model.x[1]>=0, model.x[2]>=0))
model.compl.add(complements(model.x[2]>=0, model.x[3]>=0))
model.compl.add(complements(model.x[3]>=0, model.x[4]>=0))
model.compl.add(complements(model.x[4]>=0, model.x[5]>=0))
This component defines a list of complementarity conditions. The list index can be
used in Pyomo, but this component simplifies the declaration of models for which
the index values are not important. The ComplementarityList component can
also be defined with a rule iteratively yielding complementarity conditions:
# ex1c.py
import pyomo.environ as pyo
from pyomo.mpec import ComplementarityList, complements
n = 5
model = pyo.ConcreteModel()
13.3 MPEC Transformations 203
model.f = pyo.Objective(expr=sum(i*(model.x[i]-1)**2
for i in range(1,n+1)) )
def compl_(model):
yield complements(model.x[1] >= 0, model.x[2] >= 0)
yield complements(model.x[2] >= 0, model.x[3] >= 0)
yield complements(model.x[3] >= 0, model.x[4] >= 0)
yield complements(model.x[4] >= 0, model.x[5] >= 0)
model.compl = ComplementarityList( rule=compl_ )
Similarly, the construction rule may be a Python list comprehension that gener-
ates a sequence of complementarity conditions:
# ex1e.py
import pyomo.environ as pyo
from pyomo.mpec import ComplementarityList, complements
n = 5
model = pyo.ConcreteModel()
model.f = pyo.Objective(expr=sum(i*(model.x[i]-1)**2
for i in range(1,n+1)) )
model.compl = ComplementarityList(
rule=(complements(model.x[i] >= 0, model.x[i+1] >= 0)
for i in range(1,n)) )
Pyomo supports the automated transformation of models. Pyomo can iterate through
model components as well as nested model blocks. Thus, model components can
be easily transformed locally, and global data can be collected to support global
transformations. Further, Pyomo components and blocks can be activated and deac-
tivated, which facilitates in place transformations that do not require the creation of
a separate copy of the original model.
Pyomo’s pyomo.mpec package defines several model transformations that can
be easily applied. For example, if model defines an MPEC model (as in our previ-
ous examples), then the following example illustrates how to apply a model trans-
formation:
xfrm = pyo.TransformationFactory("mpec.simple_nonlinear")
transformed = xfrm.create_using(model)
l1 ≤ expr1 ≤ u1 ⊥ l2 ≤ expr2 ≤ u2 ,
where exactly two of the constant bounds l1 , u1 , l2 and u2 are finite. The non-finite
bounds are typically omitted, but the value None can be used to express infinite
bounds. Additionally, each constraint expression can be expressed with a simple
inequality of the form
expr3 ≤ expr4 .
The mpec.standard form transformation reformulates each complementar-
ity condition in a model into a standard form:
l1 ≤ expr ≤ u1 ⊥ l2 ≤ var ≤ u2 ,
where exactly two of the constant bounds l1 , u1 , l2 and u2 are finite, and either l2 is
zero or both l2 or u2 are finite.
Note that this transformation creates new variables and constraints as part of this
transformation. For example, the complementarity condition
1 ≤ x + y ⊥ 1 ≤ 2x − y,
is transformed to:
1 ≤ x + y ⊥ 0 ≤ v,
where v ∈ R and v = 2x − y − 1.
For each complementary condition object, the new variable and constraints are
added as additional components within the complementarity object. Thus, the over-
all structure of the MPEC model is not changed by this transformation.
(expr − l1 ) ∗ v ≤ ε
(u1 − expr) ∗ v ≤ ε
• If l2 and u2 are both finite, then the following constraints are defined:
(var − l2 ) ∗ expr ≤ ε
(var − u2 ) ∗ expr ≤ ε
Each of these cases ensure the complementarity condition is met when ε is zero. For
example, in the first case, we know 0 ≤ v and 0 ≤ expr − l1 . When ε is zero, this
constraint ensures either v is zero or expr − l1 is zero.
This transformation uses the parameter mpec bound, which defines the value
for ε for every complementarity condition. This allows for the specification of a
relaxed nonlinear problem, which may be easier to optimize with some nonlinear
programming solvers. The default value of mpec bound is zero.
l1 ≤ expr1 ≤ u1 ⊥ l2 ≤ expr2 ≤ u2 ,
where exactly two of the constant bounds l1 , u1 , l2 and u2 are finite. Without loss of
generality, we assume that either l1 or u1 is finite.
This transformation generates the constraints corresponding to the conditions
implied by the complementarity conditions (see Equation (13.1)). There are three
different cases:
• If the first constraint is an equality, then the complementarity condition is triv-
ially replaced by that equality constraint.
• If both bounds on the first constraint are finite but different, then the disjunction
has the form:
Y1 _ Y2 _ Y3
l1 = expr1 expr1 = u1 l1 ≤ expr1 ≤ u1
expr2 ≥ 0 expr2 ≤ 0 expr2 = 0
0 ≤ expr1 ⊥ 0 ≤ expr2 ,
Y ∈ {True, False}
This transformation makes use of modeling components and transformations
from Pyomo’s pyomo.gdp package. The transformation expresses each of the dis-
junctive terms explicitly using Disjunct components and the select exactly one
logical condition using the Disjunction component. The transformation adds
the Disjunct and Disjunction components within the objects representing
the complementarity conditions. It then recasts the modified complementarity com-
ponents into simple Block components. This localizes all changes to the model to
the individual complementarity components. Subsequent transformation of the dis-
junctive expressions to algebraic constraints can be effected through either Big-M
(gdp.bigm) or Convex Hull (gdp.chull) transformations.
Solvers like PATH [14] have been tailored to work with the AMPL Solver Library
(ASL). AMPL uses nl files to communicate with solvers, which read nl files
with the ASL. Pyomo can also create nl files, and the mpec.nl transformation
processes Complementarity components into a canonical form suitable for this
format [16].
This example illustrates the use of the ipopt interior-point solver to solve a
problem generated with the mpec.simple nonlinear transformation. When
a transformation is used directly like this, the results returned to the user include
decision variables for the transformed model. Pyomo does not have general capa-
bilities for mapping a solution back into the space from the original model. In this
example, the results object includes values for the x variables as well as the vari-
ables v introduced when applying the transformation to the standard form as shown
previously.
Pyomo includes a meta-solver, mpec nlp that applies the nonlinear transfor-
mation, performs optimization, and then returns results for the original decision
variables. For example, mpec nlp executes the same logic as the previous pyomo
example:
pyomo solve --solver=mpec_nlp ex1a.py
Additionally, this meta-solver can also manipulate the ε values in the model, starting
with larger values and iteratively tightening them to generate a more accurate model.
pyomo solve --solver=mpec_nlp \
--solver-options="epsilon_initial=0.1 \
epsilon_final=1e-7" \
ex1a.py
This approach may be useful when using a nonlinear solver that has difficulty opti-
mizing with equality constraints.
Library (ASL), so mpec minlp can optimize nonlinear MPECs with a solver like
Couenne [10].
If the original model was a linear MPEC, then the resulting model is a mixed-
integer linear program able to be globally optimized (e.g., see Hu et al. [33], Júdice
[36]). For example, the pyomo command can be used to execute the mpec minlp
solver using a specified MIP solver:
pyomo solve --solver=mpec_minlp \
--solver-options="solver=glpk" ralph1.py
Note that Pyomo includes interfaces to a variety of MIP solvers, including CPLEX,
Gurobi, CBC, and GLPK.
Pyomo’s solver interface for the AMPL Solver Library (ASL) applies the mpec.nl
transformation, writes an AMPL .nl file, executes an ASL solver, and then loads
the solution into the original model. Pyomo provides a custom interface to the PATH
solver [14], which simply allows the solver to be specified as path while the solver
executable is named pathamp.
The pyomo command can execute the PATH solver by simply specifying the
path solver name. For example, consider the munson1 problem from MCPLIB:
# munson1.py
import pyomo.environ as pyo
from pyomo.mpec import Complementarity, complements
model = pyo.ConcreteModel()
model.x1 = pyo.Var()
model.x2 = pyo.Var()
model.x3 = pyo.Var()
model.f1 = Complementarity(expr=complements(
model.x1 >= 0,
model.x1 + 2*model.x2 + 3*model.x3 >= 1))
model.f2 = Complementarity(expr=complements(
model.x2 >= 0,
model.x2 - model.x3 >= -1))
model.f3 = Complementarity(expr=complements(
model.x3 >= 0,
model.x1 + model.x2 >= -1))
13.5 Discussion
Abstract This chapter provides a short tutorial of the Python programming lan-
guage. This chapter briefly covers basic concepts of Python, including variables,
expressions, control flow, functions, and classes. The goal is to provide a reference
for the Python constructs used in the book. A full introduction to Python is provided
by resources such as those listed at the end of the chapter.
A.1 Overview
211
212 A A Brief Python Tutorial
as well as a rich set of built-in standard libraries avaibable for use to quickly build
sophisticated software applications.
The goal in this Appendix is to provide a reference for Python constructs used in
the rest of the book. A full introduction to Python is provided by resources such as
those listed at the end of the chapter.
Python codes are executed using an interpreter. When this interpreter starts, a com-
mand prompt is printed and the interpreter waits for the user to enter Python com-
mands. For example, a standard way to get started with Python is to execute the
interpreter from a shell environment and then print “Hello World”:
% python
Python 3.7.4 (default, Aug 13 2020, 20:35:49)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more \
information.
>>> print ("Hello World")
Hello World
>>>
On Windows the python command can be launched from the DOS shell (or other
shells), and on *nix (which includes Macs) the python command can be launched
from a bash or csh shell (or terminal). The Python interactive shell is similar to these
shell environments; when a user enters a valid Python statement, it is immediately
evaluated and its corresponding output is immediately printed.
The interactive shell is useful for interrogating the state of complex data types. In
most cases, this will involve single-line statements, like the print function shown
in the previous example. Multi-line statements can also be entered into the interac-
tive shell. Python uses the “...” prompt to indicate that a continuation line is needed
to define a valid multi-line statement. For example, a conditional statement requires
a block of statements defined on continuation lines:
>>> x = True
>>> if x:
... print("x is True")
... else:
... print("x is False")
...
x is True
The Python interpreter can also be used to execute Python statements in a file,
which allows the automated execution of complex Python programs. Python source
files are text files, and the convention is to name source files with the .py suffix.
For example, consider the example.py file:
# This is a comment line, which is ignored by Python
print("Hello World")
The code can be executed in several ways. Perhaps the most common is to exe-
cute the Python interpreter within a shell environment:
% python example.py
Hello World
%
import sys
sys.stdin.readline()
Python does not make use of begin-end symbols for blocks of code. Instead, a colon
is used to indicate the end of a statement defining the start of a block and then
indentation is used to demarcate the block. For example, consider a file containing
the following Python commands:
# This comment is the first line of LineExample.py
# all characters on a line after the #-character are
# ignored by Python
population = 400
When passed to Python, this program will cause some text to be output.
Because indentation has meaning, Python requires consistency. The following
program will generate an error message because the indentation within the if-block
is inconsistent:
# This comment is the first line of BadIndent.py,
# which will cause Python to give an error message
# concerning indentation.
creates a variable called population, and it has the integer type because 200 is
an integer. Python is case sensitive, so the statement
Population = "More than yesterday."
would cause the variable population to have the same value as Population
and therefore the same type.
This section summarizes Python data structures helpful in scripting Pyomo appli-
cations. Many Python and Pyomo data structures can be accessed by indexing their
elements. Pyomo typically starts indices and ranges with a one, while Python is zero
based.
A.5.1 Strings
String literals can be enclosed in either single or double quotes, which enables the
other to be easily included in a string. Python supports a wide range of string func-
tions and operations. For example, the addition operator (+) concatenates strings. To
cast another type to string, use the str function. The Python line:
NameAge = "SPAM was introduced in " + str(1937)
A.5.2 Lists
NOTE: In Python, lists can have mixed types such as the mixture of floats and
integers just given.
There are many list functions. Perhaps the most common is the append func-
tion, which adds elements to the end of a list:
>>> a = []
>>> a.append(16)
>>> a.append(22.4)
>>> a
[16, 22.4]
A.5.3 Tuples
Tuples are similar to lists, but are intended to describe multi-dimensional objects.
For example, it would be reasonable to have a list of tuples. Tuples differ from lists in
that they use parentheses rather than square brackets for initialization. Additionally,
the members of a list can be changed by assignment while tuples cannot be changed
(i.e., tuples are immutable while lists are mutable). Although parentheses are used
for initialization, square brackets are used to reference individual elements in a tuple
(which is the same as for lists; this allows members of a list of tuples to be accessed
with code that looks like access to an array).
Suppose we have a tuple intended to represent the location of a point in a three-
dimensional space. The origin would be given by the tuple (0,0,0). Consider the
following Python session:
>>> orig = (0,0,0)
>>> pt = (-1.1, 9, 6)
>>> pt[1]
9
>>> pt = orig
>>> pt
(0, 0, 0)
A.5.4 Sets
Python sets are extremely similar to Pyomo Set components. Python sets cannot
have duplicate members and are unordered. They are declared using the set func-
tion, which takes a list (perhaps an empty list) as an argument. Once a set has been
created, it has member functions for operations such as add (one new member),
update (with multiple new members), and discard (existing members). The
following Python session illustrates the functionality of set objects:
>>> A = set([1, 3])
>>> B = set([2, 4, 6])
>>> A.add(7)
>>> C = A | B
>>> print(C)
set([1, 2, 3, 4, 6, 7])
NOTE: Lowercase set refers to the built-in Python object. Uppercase Set
refers to the Pyomo component.
A.5.5 Dictionaries
Python dictionaries are somewhat similar to lists; however, they are unordered and
they can be indexed by any immutable type (e.g., strings, numbers, tuples composed
of strings and/or numbers, and more complex objects). The indices are called keys,
and within any particular dictionary the keys must be unique. Dictionaries are cre-
ated using curly brackets, and they can be initialized with a list of key-value pairs
separated by commas. Dictionary members can be added by assignment of a value
to the dictionary key. The values in the dictionary can be any object (even other
dictionaries), but we will restrict our attention to simpler dictionaries. Here is an
example:
>>> D = {’Bob’:’123-1134’,}
>>> D[’Alice’] = ’331-9987’
>>> print(D)
{’Bob’: ’123-1134’, ’Alice’: ’331-9987’}
>>> print(D.keys())
[’Bob’, ’Alice’]
>>> print(D[’Bob’])
123-1134
218 A A Brief Python Tutorial
A.6 Conditionals
The elif and else statements are optional and any number of elif statements
can be used. Each conditional code block can contain an arbitrary number of state-
ments. The conditionals can be replaced by a logical expression, a call to a boolean
function, or a boolean variable (and it could even be called CONDITIONAL1). The
boolean literals True and False are sometimes used in these expressions. The
following program illustrates some of these ideas:
x = 6
y = False
if x == 5:
print("x happens to be 5")
print("for what that is worth")
elif y:
print("x is not 5, but at least y is True")
else:
print("This program cannot tell us much.")
As is typical for modern programming languages, Python offers for and while
looping as modified by continue and break statements. When an else state-
ment is given for a for or while loop, the code block controlled by the else
statement is executed when the loop terminates. The continue statement causes
the current block of code to terminate and transfers control to the loop statement.
The break command causes an exit from the entire looping construct.
The following example illustrates these constructs:
D = {’Mary’:231}
D[’Bob’] = 123
D[’Alice’] = 331
D[’Ted’] = 987
for i in sorted(D):
if i == ’Alice’:
continue
if i == ’John’:
print("Loop ends. Cleese alert!")
A.9 Functions 219
break;
print(i+" "+str(D[i]))
else:
print("Cleese is not in the list.")
In this example, the for-loop iterates over all keys in the dictionary. The in keyword
is particularly useful in Python to facilitate looping over iterable types such as lists
and dictionaries. Note that the order of keys is arbitrary; the sorted() function
can be used to sort them.
This program will print the list of keys and dictionary entries, except for the key
“Alice,” and then it prints “Cleese is not in the list.” If the name “John” was one of
the keys, the loop would terminate whenever it was encountered and in that case,
the else clause would skipped because break causes control to exit the entire
looping structure, including its else.
Generators and list comprehensions are closely related. List comprehensions are
commonly used in Pyomo models because they create a list “on-the-fly” using a
concise syntax. Generators allow iteration over a list without creating it.
Before discussing list comprehensions and generators, it is helpful to review the
Python range function. It takes up to three integer arguments: start, beyond,
and step. The range function returns a list beginning with start, adds step
to it for each element, and stops without creating beyond. The default value for
start is zero and the default value for step is one. If only one argument is given,
it is beyond. If two arguments are given, then they are start and beyond.
A list comprehension is an expression within square brackets specifying the cre-
ation of a list. The following Python session illustrates the use of a list comprehen-
sion generating the squares of the first five natural numbers:
>>> a = [i*i for i in range(1,6)]
>>> a
[1, 4, 9, 16, 25]
A.9 Functions
Python functions can take objects as arguments and return objects. Because Python
offers built-in types like tuples, lists, and dictionaries, it is easy for a function to
return multiple values in an orderly way. Writers of a function can provide default
220 A A Brief Python Tutorial
def SqifOdd(x):
# if x is odd, 2*int(x/2) is not x
# due to integer divide of x/2
if 2*int(x/2) == x:
return x
else:
return x*x
ShortList = range(4)
B = Apply(SqifOdd, ShortList)
print(B)
This program prints [0, 1, 2, 9]. The Apply function assumes it has been
passed a function and a list; it builds up a new list by applying the function to the
list and then returns the new list. The SqifOdd function returns its argument (x)
unless 2*int(x/2) is not x. If x is an odd integer, then int(x/2) will truncate
x/2 so two times the result will not be equal to x.
A somewhat advanced programming topic is the writing and use of function
wrappers. There are multiple ways to write and use wrappers in Python, but we
will now briefly introduce decorators because they are sometimes used in Pyomo
models and scripts. Although the definition of a decorator can be complicated, the
use of one is simple: an at-sign followed by the name of the decorator is placed on
the line above the declaration of the function to be decorated.
Next is an example of the definition and use of a silly decorator to change ’c’ to
’b’ in the return values of a function.
# An example of a silly decorator to change ’c’ to ’b’
# in the return value of a function.
A.10 Objects and Classes 221
def ctob_decorate(func):
def func_wrapper(*args, **kwargs):
retval = func(*args, **kwargs).replace(’c’,’b’)
return retval.replace(’C’,’B’)
return func_wrapper
@ctob_decorate
def Last_Words():
return "Flying Circus"
In the definition of the decorator, whose name is ctob decorate, the function
wrapper, whose name is func wrapper uses a fairly standard Python mechanism
for allowing arbitrary arguments. The function passed in to the formal argument
called func is assumed by the wrapper to return a string (this is not checked by the
wrapper). Once defined, the wrapper can then be used to decorate any number of
functions. In this example, the function Last Words is decorated, which has the
effect of modifying its return value.
Classes define objects. Put another way: objects instantiate classes. Objects can have
members that are data or functions. In this context, functions are often called meth-
ods. As an aside, we note that in Python both data and functions are technically
objects, so it would be correct to simply say objects can have member objects.
User-defined classes are declared using the class command and everything in
the indent block of a class command is part of the class definition. An overly simple
example of a class is a storage container printing its value:
class IntLocker:
sint = None
def __init__(self, i):
self.set_value(i)
def set_value(self, i):
if type(i) is not int:
print("Error: %d is not integer." % i)
else:
self.sint = i
def pprint(self):
print("The Int Locker has "+str(self.sint))
a = IntLocker(3)
a.pprint() # prints: The Int Locker has 3
a.set_value(5)
a.pprint() # prints: The Int Locker has 5
The class IntLocker has a member data element called sint and two member
functions. When a member function is called, Python automatically supplies the
222 A A Brief Python Tutorial
object as the first argument. Thus, it makes sense to list the first argument of a
member function as self, because this is the way a class can refer to itself. The
init method is a special member function automatically called when an object
is created; this function is not required.
New attributes can easily be attached to a Python object. For example, for an
object named a one can attach an attribute called name with the value “spam”
using:
a.name = "spam"
It is also possible to query objects to see what attributes they already have.
A.11.1 References
But a subtle point is y references the same thing x references, not x itself. So to
continue the example:
>>> x = [1,2,3]
>>> y = x
>>> x[0] = 3
>>> x = ‘‘Norwegian Blue’’
>>> print(x, y)
A few types in Python are immutable, which means their value in memory cannot
be changed; among them are int, float, decimal, bool, string, and tuple. Apart from
tuple, this is not surprising. Consider the following:
>>> x = (1,2,3)
>>> y = x
>>> x[0] = 3
A.12 Modules 223
This Python session will result in an error because tuples (unlike lists) cannot be
changed once they are created.
A.11.2 Copying
Sometimes assignment of a reference is not what is wanted. For these situations, the
Python copy module allows for transfer of data from one variable to another. It
supports shallow copies via the copy method and deep copies via the deepcopy
method. The difference is only apparent for compound structures (e.g. a dictionary
containing lists). The deepcopy method will attempt to make a new copy of ev-
erything, while copy will only make a new copy of the top level and will try to
create references for everything else. For example,
>>> import copy
>>> x = [1,2,3]
>>> y = copy.deepcopy(x)
>>> x[0] = 3
>>> x.append(6)
>>> print(x,y)
A.12 Modules
A module is a file containing Python statements. For example, any file containing a
Python “program” defining classes or functions is a module. Definitions from one
module can be made available in another module (or program file) via the import
command, which can specify which names to import or specify the import of all
names by using an asterisk.
Python is typically installed with many standard modules present, such as types.
The command from types import * causes the import of all names from the
types module.
Multiple module files in a directory can be organized into a package, and pack-
ages can contain modules and sub-packages. Imports from a package can use a state-
ment giving the package name (i.e., directory name) followed by a dot followed by
a the module name. For example, the command
import pyomo.environ as pyo
imports the environ module from the pyomo package and makes the names
in this module available through the local name pyo. Analogous to the init
224 A A Brief Python Tutorial
method in a Python class, an init .py file can be included in a directory and
any code therein is executed when that module is imported.
225
226 A A Brief Python Tutorial
[56] H. Schichl. Models and the history of modeling. In J. Kallrath, editor, Model-
ing Languages in Mathematical Optimization, Dordrecht, Netherlands, 2004.
Kluwer Academic Publishers.
[57] TOMLAB. TOMLAB optimization environment. http://www.tomopt.
com/tomlab, 2008.
[58] H. P. Williams. Model Building in Mathematical Programming. John Wiley
& Sons, Ltd., fifth edition, 2013.
[59] Y. Zhou and J. Davis. Open source software reliability model: An empirical
approach. ACM SIGSOFT Software Engineering Notes, 30:1–6, 2005.
Index
229
230 Index