In order to provide a gentle introduction to our interfaces, the examples so far have demonstrated only very basic capabilities. We will now attempt to demonstrate some of the power of our Python interface by describing a more complex example. This example is intended to capture most of the common ingredients of large, complex optimization models. Implementing this same example in another API would most likely have required hundreds of lines of code (ours is around 70 lines of Python code).
We'll need to present a few preliminaries before getting to the example itself. You'll need to learn a bit about the Python language, and we'll need to describe a few custom classes and functions. Our intent is that you will come away from this section with an appreciation for the power and flexibility of this interface. It can be used to create quite complex models using what we believe are very concise and natural modeling constructs. Our goal with this interface has been to provide something that feels more like a mathematical modeling language than a programming language API.
If you'd like to dig a bit deeper into the Python language constructs described here, we recommend that you visit the online Python tutorial.
Motivation
At the heart of any optimization model lies a set of decision variables. Finding a convenient way to store and access these variables can often represent the main challenge in implementing the model. While the variables in some models map naturally to simple programming language constructs (e.g., x[i] for contiguous integer values i), other models can present a much greater challenge. For example, consider a model that optimizes the flow of multiple different commodities through a supply network. You might have a variable x['Pens', 'Denver', 'New York'] that captures the flow of a manufactured item (pens in this example) from Denver to New York. At the same time, you might not want to have a variable x['Pencils', 'Denver', 'Seattle'], since not all combinations of commodities, source cities, and destination cities represent valid paths through the network. Representing a sparse set of decision variables in a typical programming language can be cumbersome.
To compound the challenge, you typically need to build constraints that involve subsets of these decision variables. For example, in our network flow model you might want to put an upper bound on the total flow that enters a particular city. You could certainly collect the relevant decision variables by iterating over all possible cities and selecting only those variables that capture possible flow from that source city into the desired destination city. However, this is clearly wasteful if not all origin-destination pairs are valid. In a large network problem, the inefficiency of this approach could lead to major performance issues. Handling this efficiently can require complex data structures.
The Gurobi Python interface has been designed to make the issues we've just described quite easy to manage. We'll present a specific example of how this is done shortly. Before we do, though, we'll need to describe a few important constructs: lists, tuples, dictionaries, list comprehension, and the tuplelist class. The first four are standard Python concepts that are particularly important in our interface, while the last is a custom class that we've added to the Gurobi Python interface.
A quick reminder: you can consult the online Python documentation for additional information on any of the Python data structures mentioned here.
Lists and Tuples
The list data structure is central to most Python programs; Gurobi Python programs are no exception. We'll also rely heavily on a similar data structure, the tuple. Tuples are crucial to providing efficient and convenient access to Gurobi decision variables in Gurobi Python programs. The difference between a list and a tuple is subtle but important. We'll discuss it shortly.
Lists and tuples are both simply ordered collections of Python objects. A list is created and displayed as a comma-separated list of member objects, enclosed in square brackets. A tuple is similar, except that the member objects are enclosed in parenthesis. For example, [1, 2, 3] is a list, while (1, 2, 3) is a tuple. Similarly, ['Pens', 'Denver', 'New York'] is a list, while ('Pens', 'Denver', 'New York') is a tuple.
You can retrieve individual entries from a list or tuple using square brackets and zero-based indices:
gurobi> l = [1, 2.0, 'abc'] gurobi> t = (1, 2.0, 'abc') gurobi> print l[0] 1 gurobi> print t[1] 2.0 gurobi> print l[2] abc
What's the difference between a list and a tuple? A tuple is immutable, meaning that you can't modify it once it has been created. By contrast, you can add new members to a list, remove members, change existing members, etc. This immutable property allows you to use tuples as indices for dictionaries.
Dictionaries
A Python dictionary allows you to map arbitrary key values to pieces of data. Any immutable Python object can be used as a key: an integer, a floating-point number, a string, or even a tuple.
To give an example, the following statements create a dictionary x, and then associates a value 1 with key ('Pens', 'Denver', 'New York')
gurobi> x = {} # creates an empty dictionary gurobi> x[('Pens', 'Denver', 'New York')] = 1 gurobi> print x[('Pens', 'Denver', 'New York')] 1Python allows you to omit the parenthesis when accessing a dictionary using a tuple, so the following is also valid:
gurobi> x = {} gurobi> x['Pens', 'Denver', 'New York'] = 2 gurobi> print x['Pens', 'Denver', 'New York'] 2We've stored integers in the dictionary here, but dictionaries can hold arbitrary objects. In particular, they can hold Gurobi decision variables:
gurobi> x['Pens', 'Denver', 'New York'] = model.addVar() gurobi> print x['Pens', 'Denver', 'New York'] <gurobi.Var *Awaiting Model Update*>
To initialize a dictionary, you can of course simply perform assignments for each relevant key:
gurobi> values = {} gurobi> values['zero'] = 0 gurobi> values['one'] = 1 gurobi> values['two'] = 2You can also use the Python dictionary initialization construct:
gurobi> values = { 'zero': 0, 'one': 1, 'two': 2 } gurobi> print values['zero'] 0 gurobi> print values['one'] 1
We have included a utility routine in the Gurobi Python interface that simplifies dictionary initialization for a case that arises frequently in mathematical modeling. The multidict function allows you to initialize one or more dictionaries in a single statement. The function takes a dictionary as its argument, where the value associated with each key is a list of length n. The function splits these lists into individual entries, creating n separate dictionaries. The function returns a list. The first result is the list of shared key values, followed by the n individual dictionaries:
gurobi> names, lower, upper = multidict({ 'x': [0, 1], 'y': [1, 2], 'z': [0, 3] }) gurobi> print names ['x', 'y', 'z'] gurobi> print lower {'x': 0, 'y': 1, 'z': 0} gurobi> print upper {'x': 1, 'y': 2, 'z': 3}Note that you can also apply this function to a dictionary where each key maps to a scalar value. In that case, the function simply returns the list of keys as the first result, and the original dictionary as the second.
You will see this function in several of our Python examples.
List comprehension
List comprehension is an important Python feature that allows you to build lists in a concise fashion. To give a simple example, the following list comprehension builds a list containing the squares of the numbers from 1 through 5:
gurobi> print [x*x for x in [1, 2, 3, 4, 5]] [1, 4, 9, 16, 25]A list comprehension can contain more than one for clause, and it can contain one or more if clauses. The following example builds a list of tuples containing all x,y pairs where x and y are both less than 3 and are not equal:
gurobi> print [(x,y) for x in range(3) for y in range(3) if x != y] [(0, 1), (0, 2), (1, 0), (1, 2) (2, 0), (2, 1)](Details on the range function can be found here). List comprehension is used extensively in our Python examples.
The tuplelist class
The final important item we would like to discuss is the tuplelist class. This is a custom sub-class of the Python list class that is designed to allow you to efficiently build sub-lists from a list of tuples. To be more specific, you can use the select method on a tuplelist object to retrieve all tuples that match one or more specified values in specified fields.
Let us give a simple example. We'll begin by creating a simple tuplelist (by passing a list of tuples to the constructor):
gurobi> l = tuplelist([(1, 2), (1, 3), (2, 3), (2, 4)])To select a sub-list where particular tuple entries match desired values, you specify the desired values as arguments to the select method. The number of arguments to select is equal to the number of entries in the members of the tuplelist (they should all have the same number of entries). You use a '*' string to indicate that any value is acceptable in that position in the tuple.
Each tuple in our example contains two entries, so we can perform the following selections:
gurobi> print l.select(1, '*') [(1, 2), (1, 3)] gurobi> print l.select('*', 3) [(1, 3), (2, 3)] gurobi> print l.select(1, 3) [(1, 3)] gurobi> print l.select('*', '*') [(1, 2), (1, 3), (2, 3), (2, 4)]
You may have noticed that similar results could have been achieved using list comprehension. For example:
gurobi> print l.select(1, '*') [(1, 2), (1, 3)] gurobi> print [(x,y) for x,y in l if x == 1] [(1, 2), (1, 3)]The problem is that the latter statement considers every member in the list, which can be quite inefficient for large lists. The select method builds internal data structures that make these selections quite efficient.
Note that tuplelist is a sub-class of list, so you can use the standard list methods to access or modify a tuplelist:
gurobi> print l[1] (1,3) gurobi> l += [(3, 4)] gurobi> print l [(1, 2), (1, 3), (2, 3), (2, 4), (3, 4)]
Returning to our network flow example, once we've built a tuplelist containing all valid commodity-source-destination combinations on the network (we'll call it flows), we can select all arcs that flow into a specific destination city as follows:
gurobi> inbound = flows.select('*', '*', 'New York')
We now present an example that illustrates the use of all of the concepts discussed so far.
netflow.py example
Our example solves a multi-commodity flow model on a small network. In the example, two commodities (Pencils and Pens) are produced in two cities (Detroit and Denver), and must be shipped to warehouses in three cities (Boston, New York, and Seattle) to satisfy given demand. Each arc in the transportation network has a cost associated with it, and a total capacity.
This is the complete source code for our example (also available in <installdir>/examples/python/netflow.py)...
from gurobipy import * # Model data commodities = ['Pencils', 'Pens'] nodes = ['Detroit', 'Denver', 'Boston', 'New York', 'Seattle'] arcs, capacity = multidict({ ('Detroit', 'Boston'): 100, ('Detroit', 'New York'): 80, ('Detroit', 'Seattle'): 120, ('Denver', 'Boston'): 120, ('Denver', 'New York'): 120, ('Denver', 'Seattle'): 120 }) arcs = tuplelist(arcs) cost = { ('Pencils', 'Detroit', 'Boston'): 10, ('Pencils', 'Detroit', 'New York'): 20, ('Pencils', 'Detroit', 'Seattle'): 60, ('Pencils', 'Denver', 'Boston'): 40, ('Pencils', 'Denver', 'New York'): 40, ('Pencils', 'Denver', 'Seattle'): 30, ('Pens', 'Detroit', 'Boston'): 20, ('Pens', 'Detroit', 'New York'): 20, ('Pens', 'Detroit', 'Seattle'): 80, ('Pens', 'Denver', 'Boston'): 60, ('Pens', 'Denver', 'New York'): 70, ('Pens', 'Denver', 'Seattle'): 30 } inflow = { ('Pencils', 'Detroit'): 50, ('Pencils', 'Denver'): 60, ('Pencils', 'Boston'): -50, ('Pencils', 'New York'): -50, ('Pencils', 'Seattle'): -10, ('Pens', 'Detroit'): 60, ('Pens', 'Denver'): 40, ('Pens', 'Boston'): -40, ('Pens', 'New York'): -30, ('Pens', 'Seattle'): -30 } # Create optimization model m = Model('netflow') # Create variables flow = {} for h in commodities: for i,j in arcs: flow[h,i,j] = m.addVar(ub=capacity[i,j], obj=cost[h,i,j], name='flow_%s_%s_%s' % (h, i, j)) m.update() # Arc capacity constraints for i,j in arcs: m.addConstr(quicksum(flow[h,i,j] for h in commodities) <= capacity[i,j], 'cap_%s_%s' % (i, j)) # Flow conservation constraints for h in commodities: for j in nodes: m.addConstr( quicksum(flow[h,i,j] for i,j in arcs.select('*',j)) + inflow[h,j] == quicksum(flow[h,j,k] for j,k in arcs.select(j,'*')), 'node_%s_%s' % (h, j)) # Compute optimal solution m.optimize() # Print solution if m.status == GRB.status.OPTIMAL: for h in commodities: print '\nOptimal flows for', h, ':' for i,j in arcs: if flow[h,i,j].x > 0: print i, '->', j, ':', flow[h,i,j].x
Example details
Let us now walk through the example, line by line, to understand how it achieves the desired result of computing the optimal network flow. As with the simple Python example presented earlier, this example begins by importing the Gurobi functions and classes:
from gurobipy import *
We then create a few lists that contain model data:
commodities = ['Pencils', 'Pens'] nodes = ['Detroit', 'Denver', 'Boston', 'New York', 'Seattle'] arcs, capacity = multidict({ ('Detroit', 'Boston'): 100, ('Detroit', 'New York'): 80, ('Detroit', 'Seattle'): 120, ('Denver', 'Boston'): 120, ('Denver', 'New York'): 120, ('Denver', 'Seattle'): 120 }) arcs = tuplelist(arcs)The model works with two commodities (Pencils and Pens), and the network contains 5 nodes and 6 arcs. We initialize commodities and nodes as simple Python lists. We use the Gurobi multidict function to initialize arcs (the list of keys) and capacity (a dictionary).
In our example, we plan to use arcs to select subsets of the arcs when building constraints later. We therefore pass the list of tuples returned by multidict to the tuplelist constructor to create a tuplelist object instead.
The model also requires cost data for each commodity-arc pair:
cost = { ('Pencils', 'Detroit', 'Boston'): 10, ('Pencils', 'Detroit', 'New York'): 20, ('Pencils', 'Detroit', 'Seattle'): 60, ('Pencils', 'Denver', 'Boston'): 40, ('Pencils', 'Denver', 'New York'): 40, ('Pencils', 'Denver', 'Seattle'): 30, ('Pens', 'Detroit', 'Boston'): 20, ('Pens', 'Detroit', 'New York'): 20, ('Pens', 'Detroit', 'Seattle'): 80, ('Pens', 'Denver', 'Boston'): 60, ('Pens', 'Denver', 'New York'): 70, ('Pens', 'Denver', 'Seattle'): 30 }Once this dictionary has been created, the cost of moving commodity h from node i to j can be queried as cost[(h,i,j)]. Recall that Python allows you to omit the parenthesis when using a tuple to index a dictionary, so this can be shortened to just cost[h,i,j].
A similar construct is used to initialize node demand data:
inflow = { ('Pencils', 'Detroit'): 50, ('Pencils', 'Denver'): 60, ('Pencils', 'Boston'): -50, ('Pencils', 'New York'): -50, ('Pencils', 'Seattle'): -10, ('Pens', 'Detroit'): 60, ('Pens', 'Denver'): 40, ('Pens', 'Boston'): -40, ('Pens', 'New York'): -30, ('Pens', 'Seattle'): -30 }
Building a multi-dimensional array of variables
The next step in our example (after creating an empty Model object) is to add variables to the model. The variables are stored in a dictionary flow:
flow = {} for h in commodities: for i,j in arcs: flow[h,i,j] = m.addVar(ub=capacity[i,j], cost=cost[h,i,j], name='flow_%s_%s_%s' % (h, i, j)) m.update()The flow variable is triply subscripted: by commodity, source node, and destination node. Note that the dictionary only contains variables for source, destination pairs that are present in arcs.
Arc capacity constraints
We begin with a straightforward set of constraints. The sum of the flow variables on an arc must be less than or equal to the capacity of that arc:
for i,j in arcs: m.addConstr(quicksum(flow[h,i,j] for h in commodities) <= capacity[i,j], 'cap_%s_%s' % (i, j))Note that we use list comprehension to build a list of all variables associated with an arc (i,j):
flow[h,i,j] for h in commodities(To be precise, as we've used it here, this is actually called a generator expression in Python, but it is similar enough to list comprehension that you can safely ignore the difference for the purpose of understanding this example). The result is passed into the quicksum function to create a Gurobi linear expression that captures the sum of all of these variables. The Gurobi quicksum function is an alternative to the Python sum function that is much faster for building large expressions.
Flow conservation constraints
The next set of constraints are the flow conservation constraints. They require that, for each commodity and node, the sum of the flow into the node plus the quantity of external inflow at that node must be equal to the sum of the flow out of the node:
for h in commodities: for j in nodes: m.addConstr( quicksum(flow[h,i,j] for i,j in arcs.select('*',j)) + inflow[h,j] == quicksum(flow[h,j,k] for j,k in arcs.select(j,'*')), 'node_%s_%s' % (h, j))
Results
Once we've added the model constraints, we call optimize and then output the optimal solution:
if m.status == GRB.status.OPTIMAL: for h in commodities: print '\nOptimal flows for', h, ':' for i,j in arcs: if flow[h,i,j].x > 0: print i, '->', j, ':', flow[h,i,j].x
If you run the example (gurobi.bat netflow.py on Windows, or gurobi.sh netflow.py on Linux and Mac), you should see the following output:
Optimize a model with 16 rows, 12 columns and 36 nonzeros Presolve removed 16 rows and 12 columns Presolve time: 0.00s Presolve: All rows and columns removed Iteration Objective Primal Inf. Dual Inf. Time 0 5.5000000e+03 0.000000e+00 0.000000e+00 0s Solved in 0 iterations and 0.00 seconds Optimal objective 5.500000000e+03 Optimal flows for Pencils : Detroit -> Boston : 50.0 Denver -> New York : 50.0 Denver -> Seattle : 10.0 Optimal flows for Pens : Detroit -> Boston : 30.0 Detroit -> New York : 30.0 Denver -> Boston : 10.0 Denver -> Seattle : 30.0