Skip to content

Instantly share code, notes, and snippets.

@ayhanfuat
Last active October 18, 2020 20:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ayhanfuat/5c72f6cec33fa6d50ae238d8f4345f44 to your computer and use it in GitHub Desktop.
Save ayhanfuat/5c72f6cec33fa6d50ae238d8f4345f44 to your computer and use it in GitHub Desktop.

This is an explanation for the issue Erwin Kalvelagen mentioned in his blog post.

When you add an arbitrary object to a numpy array, numpy tries to add that object to every element of the array which is known as broadcasting.

Here, for example, when you add a pulp.LpVariable to a numpy array, it is added to every element of the array:

In [36]: arr = np.array([1, 2, 3])

In [37]: x = pulp.LpVariable('x')

In [38]: arr + x
Out[38]: array([1*x + 1, 1*x + 2, 1*x + 3], dtype=object)

This behaviour is a little different when you use a list, instead of a numpy array:

In [39]: [1, 2, 3] + x

Out[39]: 1*x + 6

This is due to how the pulp.LpAffineExpression.addInPlace method is implemented:

    def addInPlace(self, other):
        if isinstance(other,int) and (other == 0): 
            return self
        if other is None: return self
        if isinstance(other,LpElement):
            self.addterm(other, 1)
        elif isinstance(other,LpAffineExpression):
            self.constant += other.constant
            for v,x in other.items():
                self.addterm(v, x)
        elif isinstance(other,dict):
            for e in other.values():
                self.addInPlace(e)
        elif (isinstance(other,list)
              or isinstance(other, Iterable)):
           for e in other:
                self.addInPlace(e)
        else:
            self.constant += other
        return self

Since the check for isinstance(other, list) returns True, it iterates over every element of the list, and adds them to x: x + 1 + 2 + 3 = x + 6.

This addInPlace method is called by the x variable's __radd__ method. __radd__ is a special method in Python that gets called when the left operand (in this case, the [1, 2, 3] list) does not now how to do addition with the right operand (the LpVariable x).

This does not happen with the numpy array because when you type arr + x, numpy assumes it knows how to add an object to an array, and starts the computation. Only when the broadcasting step is completed (i.e. when numpy has to add the first element of the array to x) the __radd__ method of x is called. If you switch the operands, you'll see something different:

In [55]: x + arr

Out[55]: 1*x + 6

Now, the result is inline with the list addition result because numpy's broadcasting doesn't happen. x sees that arr is an Iterable, and adds every element of the array to itself.


So why does something like prob += x + (arr - y) work but prob += (arr - y) doesn't? It is because of the addInPlace method again. Since arr - y returns an Iterable, when you add that to an LpVariable (x), every element of the Iterable is added to x, one by one, resulting in a single pulp expression. However, if the left operand is a numpy array, broadcasting happens and it propagates to every operation. arr - y yields an array, (arr - y) + x also yields an array, and so on. Since this does not result in a single expression, pulp cannot do optimization on it.

This indeed looks like unintended behaviour. I would follow Erwin's suggestion on not using numpy arrays with pulp or using it carefully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment