Skip to content

Slow performance instantiating large problems #749

Open
@MBradbury

Description

Details for the issue

What did you do?

Creating large problems in PULP is slow, there are a two issues:

  1. Creating expressions via pulp.lpSum is slower than directly creating them via pulp.LpAffineExpression
  2. Making constraints requires allocating and filling an additional 2 copies of an LpAffineExpression
import pulp, numpy as np, itertools

# Some reward matrix
reward = np.full((300, 200, 50), 10)

prob = pulp.LpProblem("Problem", pulp.constants.LpMaximize)
dv = pulp.LpVariable.dicts("dv",
       itertools.product(range(300), range(200), range(50)),
       cat=pulp.constants.LpBinary)

Slow objective function:

prob += pulp.lpSum([
            dv[a, b, c] * reward[a, b, c]
            for a in range(300)
            for b in range(200)
            for c in range(50)
        ])

Faster objective function:

prob += pulp.LpAffineExpression(
            (dv[a, b, c], reward[a, b, c])
            for a in range(300)
            for b in range(200)
            for c in range(50)
        )

Slow constraint:

for c in range(50):
  prob += pulp.lpSum([
              dv[a, b, c]
              for a in range(300)
              for b in range(200)
          ]) == 1

Faster constraint:

for c in range(50):
  prob += pulp.LpAffineExpression(
              (dv[a, b, c], 1)
              for a in range(300)
              for b in range(200)
          ) == 1

Making constraints is slow as self - other creates the first LpAffineExpression copy at https://github.com/coin-or/pulp/blob/master/pulp/pulp.py#L980, https://github.com/coin-or/pulp/blob/master/pulp/pulp.py#L983 or https://github.com/coin-or/pulp/blob/master/pulp/pulp.py#L986. A second copy is then made when LpConstraint's constructor is called (https://github.com/coin-or/pulp/blob/master/pulp/pulp.py#L1012).

It seems as if one of these copies can be avoided if the rhs is set when other is int/float. So in LpAffineExpression.__eq__ this would be:

def __eq__(self, other):
        if isinstance(other, (float, int)):
            return LpConstraint(self, const.LpConstraintEQ, rhs=other)
        else:
            return LpConstraint(self - other, const.LpConstraintEQ)

Avoiding the second copy of LpAffineExpression would require that LpConstraint does not inherit from LpAffineExpression, which is a more significant change.

Some performance numbers using the attached script:

Slow Objective: 16.692 seconds
Slow Constraint: 22.000 seconds
Fast Objective: 2.945 seconds
Fast Constraint: 20.278 seconds
Faster Constraint: 15.407 seconds
  • Happy to send a PR with the change to use rhs if this looks correct.
  • Unsure about the right way forward with lpSum, as LpAffineExpression's are constructed when doing the internal multiplication, ideally something more lightweight (e.g., tuple) would be used as the result of the multiplication.

Useful extra information

The info below often helps, please fill it out if you're able to. :)

What operating system are you using?

  • Windows: Microsoft Windows [Version 10.0.19045.4291]

I'm using python version: Python 3.10.12

I installed PuLP via:

  • pypi (python -m pip install pulp)

Did you also

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions