The "toehold problem" is named after a tech support response from Gurobi. The nature of the problem is that in order to take advantage of the algebraic constraint modeling provided by gurobipy
, then the Model.addConstr
function needs a "toehold" with which to build a Constr
.
(Note that Constr
is not part of the public package. You shouldn't try to build it directly, but instead let gurobipy
create it for you as part of writing out algebraic constraints).
So what do I mean, specifically? To begin, let's make a function that captures exceptions, since I'm going to be making mistakes and deliberately throwing exceptions.
In [1]:
def exception_thrown(f):
try:
f()
except Exception as e:
return str(e)
Let's make a constraint without creating any problems. (You'll need to understand lambda
to understand this code).
In [2]:
import gurobipy as gu
m = gu.Model()
v = m.addVar(name = "goodstuff")
m.update()
exception_thrown(lambda : m.addConstr(v <= 100, name = "c1"))
In [3]:
m.update()
m.getConstrs()
Out[3]:
Ok, now let's screw up and make a bad constraint. This might happen to you, so pay attention please.
In [4]:
exception_thrown(lambda : m.addConstr(0 <= 300, name = "not_going_to_be_added_to_model"))
Out[4]:
The numbers and constraint type aren't important.
In [5]:
exception_thrown(lambda : m.addConstr(10 == 30, name = "not_going_to_be_added_to_model"))
Out[5]:
Now, why would you ever try to write a dumb constraint like that? Well, it happens naturally in the real world quite easily. Suppose you were summing over a set of variables that happened to be empty as part of building a constraint.
In [6]:
exception_thrown(lambda : m.addConstr(sum(_ for x in m.getVars() if "bad" in x.VarName.lower())
<= 100, name = "not_going_to_be_added_either"))
Out[6]:
How did this happen? It's because we used sum
. This returns the number zero if it is passed an empty sequence.
In [7]:
[_ for x in m.getVars() if "bad" in x.VarName.lower()]
Out[7]:
In [8]:
sum(_ for x in m.getVars() if "bad" in x.VarName.lower())
Out[8]:
So what's the solution? Usually, it just involves using gurobipy.quicksum
.
In [9]:
gu.quicksum(_ for x in m.getVars() if "bad" in x.VarName.lower())
Out[9]:
See what happened there? gu.quicksum
will give us a toehold. It's not just faster than sum
, it's smarter too. So when we use quicksum
, the constraint can be added.
In [10]:
exception_thrown(lambda : m.addConstr(gu.quicksum(_ for x in m.getVars()
if "bad" in x.VarName.lower())
<= 100, name = "c2"))
In [11]:
m.update()
m.getConstrs()
Out[11]: