Gurobi Cookbook - Column-wise Formation (5/5)
written by Abremod, LLC on 2019-07-01
Using a column-wise formulation
Often, is it more convenient to think of the contraints that a variable participates in instead of the variables that are part of a constraint. Also, in a column-generation procedure, this is required. To support this, Gurobi has a Column object that can be used to when defining a model. Let's create the same model as before.
model = grb.Model()
c0 = model.addConstr(0, '>', 10, name='c0')
c1 = model.addConstr(0, '<', 4, name='c1')
model.update()
x = model.addVar(name='x', obj=2, column=grb.Column([1, 1], [c0, c1]))
y = model.addVar(name='y', obj=3, column=grb.Column([1, -1], [c0, c1]))
model.update()
model.optimize()
Optimize a model with 2 rows, 2 columns and 4 nonzeros
Coefficient statistics:
Matrix range [1e+00, 1e+00]
Objective range [2e+00, 3e+00]
Bounds range [0e+00, 0e+00]
RHS range [4e+00, 1e+01]
Presolve time: 0.00s
Presolved: 2 rows, 2 columns, 4 nonzeros
Iteration Objective Primal Inf. Dual Inf. Time
0 0.0000000e+00 1.000000e+01 0.000000e+00 0s
2 2.3000000e+01 0.000000e+00 0.000000e+00 0s
Solved in 2 iterations and 0.01 seconds
Optimal objective 2.300000000e+01
commentary
The column-wise approach often looks awkward. Linear programming formualtions are almost always documented in a row-wise format and it is natural to think in terms of the variables being added to constraints. Because of that, this method of creating models is often restricted to column-generation proceedures, where they are required and are probably under-utilized in other cases.
Obtaining the dual solution
When computing the solution to a linear problem (with no integer constraints), gurobi will also compute dual prices for the contraints. This has the economic interpretation as the change in the value of the objective as the right hand side of the contraint is increased or decreased, and it is used in Column Generation procedures. The attribute is Pi
constr_names = model.getAttr('ConstrName', model.getConstrs())
constr_values = model.getAttr("Pi", model.getConstrs())
zip(constr_names, constr_values)
[('c0', 2.5), ('c1', -0.5)]
commentary
In this example, the dual price of c0
($x + y \ge 10$) was 2.5. That means, increasing the right hand side from 10, to say 10.1 and resolving would increase the objective from 2.3 to at least 2.55, and reducing the right hand side from 10 to 9.9 would reduce the objective by at most .25 to at least 2.05. We say at least 2.55 because we are minimizing.
Gurobi Cookbook - Linear Model (4/5)
written by Abremod, LLC on 2019-06-30
Creating a linear programming model
We will formulate the following linear program
minimize 2 x + 3 y
subject to:
x + y >= 10
x - y <= 4
x, y >= 0
model = grb.Model()
x = model.addVar(name='x', obj=2)
y = model.addVar(name='y', obj=3)
model.update()
c0 = model.addConstr(x + y >= 10, name='c0')
c1 = model.addConstr(x - y <= 4, name='c1')
model.update()
model.optimize()
if model.solCount > 0:
print("objective = ", model.objVal)
else:
print("solution status = ", model.Status)
Optimize a model with 2 rows, 2 columns and 4 nonzeros
Coefficient statistics:
Matrix range [1e+00, 1e+00]
Objective range [2e+00, 3e+00]
Bounds range [0e+00, 0e+00]
RHS range [4e+00, 1e+01]
Presolve time: 0.00s
Presolved: 2 rows, 2 columns, 4 nonzeros
Iteration Objective Primal Inf. Dual Inf. Time
0 0.0000000e+00 1.000000e+01 0.000000e+00 0s
2 2.3000000e+01 0.000000e+00 0.000000e+00 0s
Solved in 2 iterations and 0.01 seconds
Optimal objective 2.300000000e+01
objective = 23.0
commentary
We first create a fresh model object with the Model constructor. Then we create the two variables, x
and y
. By default, Gurobi variables are restrictred to be nonnegative. Since the variables are nonnegative in our mathematical model, we don't need to set any bounds. We do set the objective function. Before adding constraints that reference the variables, we make a call to update
. Alternatively, we could have set the parameter UpdateMode
to 1.
Gurobi Cookbook - Relaxed Function (3/5)
written by Abremod, LLC on 2019-06-29
Obtaining the lp relaxation of a MIP
The function relaxed
of the model object creates a copy of the problem with the intgrality constraints relaxed.
relaxed = m.relax()
relaxed.optimize()
Optimize a model with 637 rows, 120 columns and 14280 nonzeros
Coefficient statistics:
Matrix range [1e+00, 1e+00]
Objective range [1e+00, 1e+00]
Bounds range [1e+00, 1e+00]
RHS range [1e+00, 1e+01]
Presolve time: 0.01s
Presolved: 120 rows, 757 columns, 14400 nonzeros
Iteration Objective Primal Inf. Dual Inf. Time
0 -0.0000000e+00 0.000000e+00 1.752000e+03 0s
414 1.7142857e+01 0.000000e+00 0.000000e+00 0s
Solved in 414 iterations and 0.04 seconds
Optimal objective 1.714285714e+01
commentary
The relax
function returns a fresh of the original model. The original model will still have the original constraints. You can see this by querying the attribute isMIP.
cplex users
In the cplex interactive shell, this corresponds to the command change problem lp
except that this command will modify the global model instead of returning a new model
m.isMIP, relaxed.isMIP
(1, 0)
commentary
The dual price is available only for continous models.
Attempting to query this attribute for MIPs (or an continuous model without a solution) will result in the exception
GurobiError: Unable to retrieve attribute 'Pi'
If you are unsure that the model has integer variables, you can check with m.isMIP
Gurobi Cookbook - Bulk Values (2/5)
written by Abremod, LLC on 2019-06-28
Getting values in bulk for a large number of variables or constraints
See previous blog post for the basics of Gurobi.
var_names = m.getAttr('VarName', m.getVars())
constr_names = m.getAttr('ConstrName', m.getConstrs())
commentary
For users of the compute server, this method is almost mantatory. The alternative [v.varName for v in m.getVars()]
, for example, will be orders of magnitude slower.
Getting nonzero values of the solution
In many models, only a small portion of the variables have nonzero values. In that case, it is usually more convenient to get a list of only the variables that have nonzero values.
epsilon = 1e-6
var_names = m.getAttr('VarName', dvars)
var_values = m.getAttr('X', dvars)
[(name, value) for name, value in zip(var_names, var_values)
if abs(value) >= epsilon]
[('x0', 1.0),
('x7', 1.0),
('x18', 1.0),
('x27', 1.0),
('x33', 1.0),
('x40', 1.0),
('x46', 1.0),
('x50', 1.0),
('x60', 1.0),
('x66', 1.0),
('x73', 1.0),
('x81', 1.0),
('x88', 1.0),
('x94', 1.0),
('x101', 1.0),
('x109', 1.0),
('x112', 1.0),
('x116', 1.0),
('x117', 1.0),
('x119', 1.0)]
Commentary
This solution uses the builk query mechanism m.getAttr to get the values, then test each value for nonzeroness. It is not sufficient to test for 0 exactly, as Gurobi will treat very small numbers as zero. To avoid this, we check that the absolute value of a solution is greater than some small epsilon.
Getting the contributions to the objective function
An interesting subset of variables are the ones that wind up affecting the objective function directly. To contribute to an objective, a variable must be nonzero and have a nonzero coeficient in the objective function.
obj_coeffs = m.getAttr('Obj', dvars)
[(name, obj_coeff * value)
for name, value, obj_coeff in zip(var_names, var_values, obj_coeffs)
if abs(obj_coeff * value) > epsilon]
[('x0', 1.0),
('x7', 1.0),
('x18', 1.0),
('x27', 1.0),
('x33', 1.0),
('x40', 1.0),
('x46', 1.0),
('x50', 1.0),
('x60', 1.0),
('x66', 1.0),
('x73', 1.0),
('x81', 1.0),
('x88', 1.0),
('x94', 1.0),
('x101', 1.0),
('x109', 1.0),
('x112', 1.0),
('x116', 1.0),
('x117', 1.0),
('x119', 1.0)]
Gurobi Cookbook - Basics (1/5)
written by Abremod, LLC on 2019-06-27
This is a collection of short, idiomatic python code for working with the Gurobi solver using its python interface.
from __future__ import print_function
# get a sample mps file from the benchmark repository
import requests
import os
import toolz
benchmark_url = 'http://miplib.zib.de/download/'
sample_mps = 'cov1075.mps.gz' # "timtab1.mps.gz"
infile = requests.get(benchmark_url + sample_mps)
if not os.path.exists(sample_mps):
with open(sample_mps, 'w') as outfile:
outfile.write(infile.content)
Setting up Gurobi
If you will be using the python interface, the best way to install Gurobi and keep it up to date is to use the Anaconda python distribution. After this in installed, you need to import the gurobi library. We usually use the aliases grb
for gurobipy, and GRB
for the namespace where gurobi keeps its constants.
import gurobipy as grb
GRB = grb.GRB
Checking the gurobi version
under the gurobi library, there is a gurobi class which contains some utilities that aren't part of typical modeling modeling. The gurobi object has a version function which returns a tuple containing the major, minor, and patch level of the gurobi installation.
grb.gurobi.version()
(6, 5, 2)
Reading a saved model
The gurobi python module has a read
function that will create an in-memory model from a model saved in an mps, lp, or several other formats. It can also read directly from several standard compressed formats, like, zip, gzip, and bzip.
m = grb.read(sample_mps)
commentary
In this case the variable m
is a reference to the in-memory model equivalent to what was stored in the file. Gurobi uses the extension to determine the format of the file. mps
files the best way to save a representation of the model. They can be used to compare performance of different solvers, or a single solver with different parameters. In this notebook, we are using a benchmark problem. While the mps
file can store an exact copy of the model, it is difficult for a human to read. The lp
format is more readable version of the model but it is lossy, so you may get different results reading from an lp
file, especially if you have large or small coefficients in your model.
for cplex users
If you are used to the interactive cplex, this is the rough equivalent if read problem
. The cplex interactive shell has an implicit model in its global namespace, and all actions operate on the global model. The model
Solving a model
A gurobi model object has an optimize
function that will start the solve process.
m.optimize()
Optimize a model with 637 rows, 120 columns and 14280 nonzeros
Coefficient statistics:
Matrix range [1e+00, 1e+00]
Objective range [1e+00, 1e+00]
Bounds range [1e+00, 1e+00]
RHS range [1e+00, 1e+01]
Found heuristic solution: objective 56
Presolve time: 0.01s
Presolved: 637 rows, 120 columns, 14280 nonzeros
Variable types: 0 continuous, 120 integer (120 binary)
Presolved: 120 rows, 757 columns, 14400 nonzeros
Root relaxation: objective 1.714286e+01, 414 iterations, 0.04 seconds
Nodes | Current Node | Objective Bounds | Work
Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
0 0 17.14286 0 120 56.00000 17.14286 69.4% - 0s
H 0 0 35.0000000 17.14286 51.0% - 0s
H 0 0 27.0000000 17.14286 36.5% - 0s
H 0 0 25.0000000 17.14286 31.4% - 0s
0 0 17.25000 0 109 25.00000 17.25000 31.0% - 0s
0 0 17.28125 0 112 25.00000 17.28125 30.9% - 0s
0 0 17.28125 0 111 25.00000 17.28125 30.9% - 0s
H 0 0 24.0000000 17.28125 28.0% - 0s
0 0 17.29164 0 112 24.00000 17.29164 28.0% - 0s
H 0 0 23.0000000 17.29164 24.8% - 0s
0 0 17.29941 0 111 23.00000 17.29941 24.8% - 0s
0 0 17.30239 0 109 23.00000 17.30239 24.8% - 0s
0 0 17.30241 0 111 23.00000 17.30241 24.8% - 0s
0 0 17.30959 0 109 23.00000 17.30959 24.7% - 0s
0 0 17.31158 0 112 23.00000 17.31158 24.7% - 0s
0 0 17.31295 0 108 23.00000 17.31295 24.7% - 0s
H 0 0 21.0000000 17.31295 17.6% - 0s
0 0 17.31380 0 110 21.00000 17.31380 17.6% - 0s
0 0 17.31524 0 111 21.00000 17.31524 17.5% - 0s
0 0 17.31562 0 110 21.00000 17.31562 17.5% - 0s
0 0 17.31630 0 111 21.00000 17.31630 17.5% - 0s
0 0 17.31655 0 114 21.00000 17.31655 17.5% - 0s
0 0 17.31739 0 113 21.00000 17.31739 17.5% - 0s
0 0 17.31752 0 114 21.00000 17.31752 17.5% - 0s
0 0 17.31772 0 113 21.00000 17.31772 17.5% - 0s
H 0 0 20.0000000 17.31772 13.4% - 0s
0 0 17.31792 0 113 20.00000 17.31792 13.4% - 0s
0 0 17.31878 0 113 20.00000 17.31878 13.4% - 0s
0 0 17.31933 0 112 20.00000 17.31933 13.4% - 1s
0 0 17.31990 0 114 20.00000 17.31990 13.4% - 1s
0 0 17.32008 0 112 20.00000 17.32008 13.4% - 1s
0 0 17.32011 0 112 20.00000 17.32011 13.4% - 1s
0 0 17.32057 0 112 20.00000 17.32057 13.4% - 1s
0 0 17.32068 0 113 20.00000 17.32068 13.4% - 1s
0 0 17.32111 0 112 20.00000 17.32111 13.4% - 1s
0 2 17.32111 0 112 20.00000 17.32111 13.4% - 1s
Cutting planes:
Gomory: 5
Zero half: 16
Explored 301 nodes (20763 simplex iterations) in 3.57 seconds
Thread count was 8 (of 8 available processors)
Optimal solution found (tolerance 1.00e-04)
Best objective 2.000000000000e+01, best bound 2.000000000000e+01, gap 0.0%
commentary
Gurobi will work solving the model until either
- It finds a provably optimal solution (within tolerance)
- It proves that the model in infeasible or unbounded
- It proves no solution exists with an objective better than the
cutoff
target. - It hits a limit
- Wall clock time
- Number of solutions found (MIPs only)
- Number of search nodes explored (MIPs only)
- Number of Simplex Iterations (rarely used)
- It receives an interrupt signal
- It encounters an error
cplex users
This is equivalent to solve
in interactive cplex
Checking if Gurobi found a solution
From the log, we can see that an optimal solution was found. In application code, it's important to explicitly check the model status after each solve. The cleaneast way to determine if Gurobi found a feasible solution is to query the SolCount
attribute. If the solution count is at least 1, then Gurobi will have a feasible solution available. If not, querying for the values in the solution will through an exception.
print(m.SolCount, m.Status)
8 2
The Status
attribute has good information as well, but it is not obvious from the status if there is a feasible solution available. In this case, an optimal solution. A status of optimal
or suboptimal
imply the existence of a solution and a status of loaded
, infeasible
, inf_or_unbd
, unbounded
, cutoff
, or numeric
definitely mean that no feasible solution was found. However, the statuses iteration_limit
, node_limit
, time_limit
, solution_limit
or interrupted
refer to why the solve terminated. For these cases, all you know is that the solution, if any, isn't optimal according to the optimality criteria. That's why checking the solution count is recommended.
Its always a good idea to explitly check for feasibility in any production application, even if you reliably get feasible solutions from Gurobi. Unless you have complete control over the data you receive, inevitibly your application will receive data that results in an infeasible model. If you don't check for fesibility, your application will throw the unintuive exception.
GurobiError: Unable to retrieve attribute 'X'
Getting the objective value of a solution
If a solution does exist, you will usually be interested in the objective value. Most often, the objective value, as this is usually tied to some business metric. This can be done with the ObjVal
attribute. Gurobi also computes a bound on the best possible objective in a feasible solution, which can be queried with the ObjBound
attribute.
m.ObjVal, m.ObjBound
(20.0, 20.0)
Getting solution quality information
Gurobi works with floating point arithmetic, which has inevitible round-off errors. Even if you have a pure integer problem with all integer coeficients, Gurobi still uses floating point numbers to solve the LP subproblems. Gurobi can produce solutions that mathematically violate constraints in what are usually very small amounts. For each variable bound, and each constraint, there is potentially a small violation. By default, Gurobi allows variables and constraints to be violated by 0.000001, but for most problems, the violation is much smaller.
m.BoundVioSum, m.ConstrVioSum, m.BoundVio, m.ConstrVio
(0.0, 0.0, 0.0, 0.0)
Getting solution values
Of course, you will want to get the actual values of the solution. In any non-trivial model, there will be a set of decision variables. The complete list of deicision variables can be obtained using the function getVars
. If the variables have all been given unique names, you can get an individual variable with the getVarByName
function. Values can be obtained by querying the X
attribute of a variable. The name of the variable.
dvars = m.getVars()
dvars[0].VarName, dvars[0].X
('x0', 1.0)