The toehold problem

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]:
[<gurobi.Constr c1>]

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]:
"unsupported operand type(s) for -: 'bool' and 'NoneType'"

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]:
"unsupported operand type(s) for -: 'bool' and 'NoneType'"

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]:
"unsupported operand type(s) for -: 'bool' and 'NoneType'"

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]:
0

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]:
<gurobi.LinExpr: 0.0>

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]:
[<gurobi.Constr c1>, <gurobi.Constr c2>]

Summary

Use quicksum instead of sum when building constraints. If you get an exception throw when building constraints that involves a message like "unsupported operand type(s) for -: 'bool' and 'NoneType'", suspect the toehold problem.