Tutorial 03
Tutorial 03
Ryo Kimura
April 3, 2019
3 Basic Modeling
In this module we will briefly review the fundamentals of MIP models, and provide examples of
how to implement them using Gurobi and CPLEX.
NOTE From this point forward, we will assume that your primary programming environment is
Ubuntu Linux; if you are using a different programming environment, you will need to determine
what changes are necessary in order to make it run. If you have trouble, you can contact the
instructor for advice on what changes to make.
On the other hand, we will still cover how to model things using both Gurobi and CPLEX, via
both Python and C++.
3.1 IP Review
A mixed integer programming (MIP) model is defined by the following characteristics:
• Linear objective function
• Affine inequality constraints
• Integrality constraints
A simple example of a MIP model is the 0-1 knapsack problem. We have n different objects,
each with a value cj and a weight aj , and a knapsack with total capacity b. Our goal is to determine
which objects to put in our knapsack so that we maximize their total value, subject to the constraint
that the total weight of our objects does not exceed the capacity of the knapsack. Let xj = 1 if
object j is included in our knapsack and 0 otherwise. Then we can represent our problem as:
n
X
max cj x j
x
j=1
Xn
s.t. aj x j ≤ b
j=1
xj ∈ {0, 1} ∀j = 1, . . . , n
where the domain constraint xj ∈ {0, 1} is implicitly represented as the combination of the affine
inequality constraints 0 ≤ xj ≤ 1 and the integrality constraint xj ∈ Z.
A more involved example of a MIP model is the following single machine scheduling problem
with weighted tardiness objective. We have n jobs that need to be run on some machine. Each job
has the following attributes:
1
1. weight wj : how important the job is,
Thus, if we start executing job j at start time Sj , we will finish executing the job at its completion
time Cj := Sj + pj . In this case, the tardiness of job j is defined to be
Tj := max{Cj − dj , 0},
i.e., it is 0 if job j finishes on time, and otherwise is the difference between the completion time
and P deadline of job j. Our goal is to minimize the weighted sum of tardiness over all jobs (i.e.,
min j∈J wj Tj ), subject to the constraints that we can only run one job at a time (since we only
have one machine), and the earliest we can start running jobs is at time 0.
We can model this problem as a MIP as follows: P let xij = 1 if job i is executed before job j
and 0 if job j is executed before job i. Let M := j pj so that Cj − M ≤ 0 for any reasonable
completion time for any job j. Then we can represent our problem as
X
min wj Tj
S,T,x
j∈J
s.t. Sj ≥ Si + pi − M (1 − xij ) ∀(i, j) ∈ Pairs(J )
Si ≥ Sj + pj − M xij ∀(i, j) ∈ Pairs(J )
Tj ≥ S j + pj − dj ∀j ∈ J
xij ∈ {0, 1} ∀(i, j) ∈ Pairs(J )
Sj , Tj ≥ 0 ∀j ∈ J
where J = {1, . . . , n} is the set of n jobs and Pairs(J ) = {(i, j) ∈ J × J | i < j} is the set of all
pairs of jobs in J .
It is helpful to think of a MIP model as being made up of four elements: parameters, variables,
the objective function, and constraints. In the previous model, these elements were:
• Parameters: wj , pj , dj , which are defined in the problem, and M , a big-M parameter calcu-
lated based on pj ; note that parameters are constant w.r.t. the variables (e.g., the value
of M does not depend on the value of xij , Sj , or Tj )
• Variables: xij , which are integer (in particular, binary) variables, and Sj and Tj , which are
(nonnegative) continuous variables
P
• Objective Function: j∈J wj Tj , which is a linear function in the variable Tj
Modern MIP solvers like Gurobi and CPLEX use a combination of branch-and-bound, general
purpose cutting planes, primal heuristics, and presolve reductions, among other things, to solve
MIPs. Later in the course, we will discuss how to manipulate these solver features; for now, it
suffices to note that modern MIP solvers can often find the optimal solutions to MIPs of moderate
size in a reasonable amount of time.
2
3.2 Gurobi Example (Python)
We will use Example 3.6.3 from [1], which uses the last MIP model we discussed, as our running
example. We can use Gurobi’s Python API to implement this MIP model. The basic structure of
such a program consists of the following steps:
1. Specify which interpreter to use (line 1): Specify the path to the Python interpreter that
will be used to run this program.
2. Import the gurobipy package (line 2): Import functions from the gurobipy package we
will use to model our problem.
3. Set up the problem parameters (lines 6-12): Defining and populating data structures
containing parameter data makes it easier to refer to them later on when we specify the
model. Lists and dicts are especially recommended due to their flexibility.
4. Set up the model (lines 15-37): Create a Model object and add variables, constraints, and
an objective function to it.
5. Solve the model (lines 39-44): Use model.optimize() to solve the model, and re-solve it
if we end with ambiguous solver status.
6. Display the solution (lines 46-53): Report optimal solution if one was found
In addition, our program also features:
1. Status descriptions (line 3): Define a dict mapping status enums (i.e., integers) to more
human-readable strings.
2. Script function (lines 5, 58, 59): Define primary code as a function, then call it from “main”
function. This makes it easier to test/parameterize our code later on.
3. Exception Handling (lines 14, 55, 56): Use a try-catch block to handle solver errors.
Here is the full Python code for our model. The program can also be found on Canvas with
some of the lines adjusted. See the examples/python directory of the Gurobi distribution for
more Python examples distributed by Gurobi, and see http://www.gurobi.com/documentation/8.1/
refman/py python api details.html for details on the Python API.
1 #!/usr/bin/python3
2 from gurobipy import Model, GRB, GurobiError, quicksum
3 StatusDict = {getattr(GRB.Status, s): s for s in dir(GRB.Status) if s.isupper()}
4
5 def TWTgurobi():
6 # TWT Problem Data
7 jobs = tuple([i+1 for i in range(4)])
8 jobPairs = [(i,j) for i in jobs for j in jobs if i < j]
9 weight = dict(zip(jobs, (4, 5, 3, 5)))
10 duration = dict(zip(jobs, (12, 8, 15, 9)))
11 deadline = dict(zip(jobs, (16, 26, 25, 27)))
12 M = sum(duration.values())
13
14 try:
3
15 # Create a new model
16 m = Model('TWTexample')
17
18 # Create variables
19 # x[(i,j)] = 1 if i << j, else j >> i
20 x = m.addVars(jobPairs, vtype=GRB.BINARY, name='x')
21 startTime = m.addVars(jobs, name='startTime')
22 tardiness = m.addVars(jobs, name='tardiness')
23
28 # Add constraints
29 m.addConstrs(
30 (startTime[j] >= startTime[i] + duration[i] - M*(1-x[(i,j)])
31 for (i,j) in jobPairs), 'NoOverlap1')
32 m.addConstrs(
33 (startTime[i] >= startTime[j] + duration[j] - M*x[(i,j)]
34 for (i,j) in jobPairs), 'NoOverlap2')
35 m.addConstrs(
36 (tardiness[j] >= startTime[j] + duration[j] - deadline[j]
37 for j in jobs), 'Deadline')
38
39 # Solve model
40 m.optimize()
41 if m.status == GRB.Status.INF_OR_UNBD:
42 # Disable dual reductions to determine solve status
43 m.setParam(GRB.Param.DualReductions, 0)
44 m.optimize()
45
46 # Display solution
47 if m.status == GRB.Status.OPTIMAL:
48 for v in m.getVars():
49 print('%s:\t%g' % (v.varName, v.x))
50 print('Objective:\t%g' % m.objVal)
51 else:
52 statstr = StatusDict[m.status]
53 print('Optimization was stopped with status %s' % statstr)
54
55 except GurobiError as e:
56 print('Error code ' + str(e.errno) + ": " + str(e))
57
58 if __name__ == '__main__':
59 TWTgurobi()
4
1. Specify which interpreter to use (line 1): Specify the path to the Python interpreter that
will be used to run this program.
2. Import the docplex/docloud package (lines 2-3): Import functions from the docplex
and docloud packages we will use to model our problem.
3. Set up the problem parameters (lines 6-12): Defining and populating data structures
containing parameter data makes it easier to refer to them later on when we specify the
model. Lists and dicts are especially recommended due to their flexibility.
4. Set up the model (lines 15-34): Create a Model object and add variables, constraints, and
an objective function to it.
5. Solve the model (lines 36-42): Use model.solve() to solve the model, and re-solve it if we
end with ambiguous solver status.
6. Display the solution (lines 44-51): Report optimal solution if one was found
1. Context Management (line 15): using with means that the Model object is automatically
deleted when its corresponding code block finishes execution
2. Script function (lines 5,53,54): Define primary code as a function, then call it from “main”
function. This makes it easier to test/parameterize our code later on.
Here is the full Python code for our model. The program can also be found on Canvas with some
of the lines adjusted. See the python/examples/mp/modeling directory of the CPLEX distribution
for more Python examples distributed by CPLEX, and see http://ibmdecisionoptimization.github.io/
docplex-doc/mp/refman.html for details on the DOcplex API.
1 #!/usr/bin/python3
2 from docplex.mp.model import Model
3 from docloud.status import JobSolveStatus
4
5 def TWTdocplex():
6 # TWT Problem Data
7 jobs = tuple([i+1 for i in range(4)])
8 jobPairs = [(i,j) for i in jobs for j in jobs if i < j]
9 weight = dict(zip(jobs, (4, 5, 3, 5)))
10 duration = dict(zip(jobs, (12, 8, 15, 9)))
11 deadline = dict(zip(jobs, (16, 26, 25, 27)))
12 M = sum(duration.values())
13
5
22 # Set objective function
23 m.minimize(m.sum(weight[j]*tardiness[j] for j in jobs))
24
25 # Add constraints
26 m.add_constraints(
27 (startTime[j] >= startTime[i] + duration[i] - M*(1-x[(i,j)]),
28 'NoOverlap1_%d_%d' % (i,j)) for (i,j) in jobPairs)
29 m.add_constraints(
30 (startTime[i] >= startTime[j] + duration[j] - M*x[(i,j)],
31 'NoOverlap2_%d_%d' % (i,j)) for (i,j) in jobPairs)
32 m.add_constraints(
33 (tardiness[j] >= startTime[j] + duration[j] - deadline[j],
34 'Deadline_%d' % j) for j in jobs)
35
36 # Solve model
37 m.solve()
38 mstatus = m.get_solve_status()
39 if mstatus == JobSolveStatus.INFEASIBLE_OR_UNBOUNDED_SOLUTION:
40 # Disable primal/dual reductions to determine solve status
41 m.parameters.preprocessing.reduce = False
42 mstatus = m.get_solve_status()
43
44 # Display solution
45 if mstatus == JobSolveStatus.OPTIMAL_SOLUTION:
46 for v in m.iter_variables():
47 print('%s:\t%g' % (v.name, v.solution_value))
48 print('Objective:\t%g' % m.objective_value)
49 else:
50 statstr = m.solve_details.status
51 print('Optimization was stopped with status %s' % str(statstr))
52
53 if __name__ == '__main__':
54 TWTdocplex()
1. Include relevant libraries (lines 1-7): Specify libraries that we will use, including the
Gurobi C++ API.
2. Set up the problem parameters (lines 15-24): We primarily use const std::array due
to its standardized design.
3. Set up the model (lines 26-93): Construct the Model object. We use std::ostringstream
throughout to construct the name strings.
6
(c) Set objective function (lines 63-69)
(d) Add constraints (lines 71-93): We use a range-based for loop to iterate over the ele-
ments of GRBVarPairMap.
4. Solve the model (lines 95-101): Use model.optimize() to solve the model, and re-solve it
if we end with ambiguous solver status.
5. Display the solution (lines 103-120): Report optimal solution if one was found, and solver
status otherwise
1. Typedefs (lines 8-9): We use typedefs to assign convenient names to our template classes.
(a) Map Template (line 8): We use the std::map template to define a data structure that
uses a key-value representation rather than a matrix-based reprsentation.
(b) Smart Pointer (line 9): We use the std::unique ptr template to automatically handle
the dynamic memory allocation of GRBVar[]
2. Exception Handling (lines 13-14, 121-130): Use a try-catch block to handle solver errors.
Here is the full C++ code for our model. The program can also be found on Canvas with some
of the lines adjusted. See the examples/cpp directory of the Gurobi distribution for more C++
examples distributed by Gurobi, and see http://www.gurobi.com/documentation/current/refman/
cpp api details.html for details on the C++ API.
1 #include <tuple>
2 #include <map>
3 #include <memory>
4 #include <array>
5 #include <vector>
6 #include <sstream>
7 #include "gurobi_c++.h"
8 typedef std::map<std::tuple<int,int>,GRBVar> GRBVarPairMap;
9 typedef std::unique_ptr<GRBVar[]> GRBVarArray;
10
7
26 // Create a new model
27 GRBEnv env = GRBEnv();
28 GRBModel model = GRBModel(env);
29 model.set(GRB_StringAttr_ModelName, "TWTexample");
30
31 // Create variables
32 // x(i)(j) = 1 if i << j, else j >> i
33 GRBVarPairMap x;
34 for (int i = 0; i < nbJobs; ++i)
35 {
36 for (int j = i+1; j < nbJobs; ++j)
37 {
38 std::ostringstream varname;
39 varname << "x(" << i << ")(" << j << ")";
40 x.emplace(std::make_tuple(i, j),
41 model.addVar(0.0, 1.0, 0.0, GRB_BINARY, varname.str()));
42 }
43 }
44 std::vector<GRBVar> startTime;
45 startTime.reserve(nbJobs);
46 for (int j = 0; j < nbJobs; ++j)
47 {
48 std::ostringstream varname;
49 varname << "startTime(" << j << ")";
50 startTime.emplace_back(
51 model.addVar(0.0, GRB_INFINITY, 0.0, GRB_CONTINUOUS, varname.str()));
52 }
53 std::vector<GRBVar> tardiness;
54 tardiness.reserve(nbJobs);
55 for (int j = 0; j < nbJobs; ++j)
56 {
57 std::ostringstream varname;
58 varname << "tardiness(" << j << ")";
59 tardiness.emplace_back(
60 model.addVar(0.0, GRB_INFINITY, 0.0, GRB_CONTINUOUS, varname.str()));
61 }
62
71 // Add constraints
72 for (auto& kv : x)
73 {
74 int i, j;
75 GRBVar x_ij;
8
76 std::forward_as_tuple(std::tie(i, j), x_ij) = kv;
77
78 std::ostringstream consname;
79 consname << "NoOverlap1_" << i << "_" << j;
80 model.addConstr(startTime[j] >= startTime[i] + duration[i] - M*(1-x_ij),
81 consname.str());
82 consname.str("");
83 consname << "NoOverlap2_" << i << "_" << j;
84 model.addConstr(startTime[i] >= startTime[j] + duration[j] - M*x_ij,
85 consname.str());
86 }
87 for (int i = 0; i < nbJobs; ++i)
88 {
89 std::ostringstream consname;
90 consname << "Deadline_" << i;
91 model.addConstr(tardiness[i] >= startTime[i] + duration[i] - deadline[i],
92 consname.str());
93 }
94
95 // Solve model
96 model.optimize();
97 if (model.get(GRB_IntAttr_Status) == GRB_INF_OR_UNBD)
98 {
99 model.set(GRB_IntParam_DualReductions, 0);
100 model.optimize();
101 }
102
9
126 }
127 catch (...)
128 {
129 std::cout << "Exception during optimization" << std::endl;
130 }
131 return 0;
132 }
10
1 #include <tuple>
2 #include <map>
3 #include <sstream>
4 #include <ilcplex/ilocplex.h>
5 typedef std::map<std::tuple<IloInt,IloInt>,IloBoolVar> IloBoolVarPairMap;
6
22 // Create variables
23 // x(i)(j) = 1 if i << j, else j >> i
24 IloBoolVarPairMap x;
25 for (IloInt i = 0; i < nbJobs; ++i)
26 {
27 for (IloInt j = i+1; j < nbJobs; ++j)
28 {
29 std::ostringstream varname;
30 varname << "x(" << i << ")(" << j << ")";
31 x.emplace(std::make_tuple(i, j), IloBoolVar(env, varname.str().c_str()));
32 }
33 }
34 IloNumVarArray startTime(env, nbJobs, 0.0, IloInfinity);
35 startTime.setNames("startTime");
36 IloNumVarArray tardiness(env, nbJobs, 0.0, IloInfinity);
37 tardiness.setNames("tardiness");
38
42 // Add constraints
43 for (auto& kv : x)
44 {
45 IloInt i, j;
46 IloBoolVar x_ij;
47 std::forward_as_tuple(std::tie(i, j), x_ij) = kv;
48
11
51 consname << "NoOverlap1_" << i << "_" << j;
52 ovCons1.setName(consname.str().c_str());
53 model.add(ovCons1);
54
70 // Solve model
71 IloCplex cplex(model);
72 cplex.solve();
73 if (cplex.getStatus() == IloAlgorithm::InfeasibleOrUnbounded)
74 {
75 cplex.setParam(IloCplex::Param::Preprocessing::Reduce, 0);
76 cplex.solve();
77 }
78
79 // Display solution
80 if (cplex.getStatus() == IloAlgorithm::Optimal)
81 {
82 for (IloIterator<IloNumVar> it(env); it.ok(); ++it)
83 {
84 IloNumVar var = *it;
85 if (cplex.isExtracted(var))
86 {
87 std::string varname = var.getName();
88 double varval = cplex.getValue(var);
89 env.out() << varname << ":\t" << varval << std::endl;
90 }
91 }
92 env.out() << "Objective:\t" << cplex.getObjValue() << std::endl;
93 }
94 else
95 {
96 IloAlgorithm::Status status = cplex.getStatus();
97 env.out() << "Optimization was stopped with status " << status << std::endl;
98 }
99 }
100 catch (const IloException& ex)
12
101 {
102 std::cerr << "CPLEX Error: " << ex << std::endl;
103 }
104 catch (...)
105 {
106 std::cerr << "Error" << std::endl;
107 }
108 env.end();
109 return 0;
110 }
Modules, Packages, and Objects First, it’s useful to clarify the distinction between a module,
a package, and a object:
• A package is any folder containing a file named init .py in it. Its name is the name of
the folder.
– In Python 3.3 and above, any folder (even without a init .py file) is considered a
package
Thus, a module is a Python file that implements various objects, while a package is conceptually a
collection of modules bundled together for distribution. By default, an object defined in a module
cannot be directly accessed from the package containing that module, unless the creator of the
package makes it available via the package’s init .py file.
Where does Python look for modules/packages? When a module named spam is imported,
Python first searches for a built-in module (i.e., a module that are compiled directly into the Python
interpreter) with that name. If not found, it then searches for a file (i.e., module) named spam.py or
a folder (i.e., package) named spam in a list of directories given by the variable sys.path. sys.path
is initialized from various locations, including the directory containing the input script, PYTHONPATH,
and the installation-dependent default.
Using Objects from the Imported Module or Package There are 4 different syntaxes for
writing import statements. (Note that importing a package is conceptually the same as importing
that package’s init .py file.)
1. import <package>
13
2. import <module>
3. from <package> import <module or subpackage or object>
4. from <module> import <object>
Let X be whatever name comes after import:
• If X is the name of a module or package, then to use objects defined in X, you have to write
X.object.
• If X is a variable name, then it can be used directly.
• If X is a function name, then it can be invoked with X().
Optionally, as Y can be added after any import X statement: import X as Y. This renames X to
Y within the script. Note that the name X itself is no longer valid. A common example is import
numpy as np.
Examples For Gurobi, gurobipy is a package which contains the object Model.
• Method 1: from gurobipy import Model
– we can directly use the object by name: m = Model()
• Method 2: from gurobipy import *
– we can directly use all objects defined in gurobipy by name; need to watch out for
naming conflicts (e.g., you cannot have a variable called Model in your script!)
• Method 3: import gurobipy as grb
– we have to prefix the object name with the (re-named) name of the module: m =
grb.Model()
For CPLEX, docplex is a package, which contains the subpackage mp, which contains the module
model which contains the object Model.
• Method 1: from docplex.mp.model import Model
– we can directly use the object by name: m = Model()
• Method 2: from docplex.mp import model or equivalently import docplex.mp.model
as model
– we have to prefix the function name with the name of the module: m = model.Model()
– This is sometimes preferred over the Method 1 in order to make it explicit that we are
using the Model object from the model module.
• Method 3: import docplex.mp.model
– we need to use the full path: m = docplex.mp.model.Model()
References
[1] Michael Pinedo. Scheduling: Theory, Algorithms, and Systems. 4th ed. Springer, 2012.
14