Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enabling the use of gurobipy directly, which should be faster #60

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions polytope/solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,17 @@
except ImportError:
logger.info('MOSEK solver not found.')

try:
import gurobipy as gp
installed_solvers.add('gurobi')
except ImportError:
logger.info('GUROBI solver not found')


# choose default from installed choices
if 'glpk' in installed_solvers:
if 'gurobi' in installed_solvers:
default_solver = 'gurobi'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lean towards not making gurobi a default solver, because gurobi and gurobipy are proprietary, closed-source software: https://www.gurobi.com/downloads/end-user-license-agreement-academic/, and: https://pypi.org/project/gurobipy/.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with not making gurobi a default solver. This is not a vote against Gurobi, but rather, automatically using a restrictive-license-encumbered dependency is an anti-pattern of open source code. Users should have to explicitly request it.

Perhaps there is a more convenient way for users to do this, e.g., an environment variable.

elif 'glpk' in installed_solvers:
default_solver = 'glpk'
elif 'scipy' in installed_solvers:
default_solver = 'scipy'
Expand All @@ -62,7 +70,6 @@
"`installed_solvers` wasn't empty above?")



def lpsolve(c, G, h, solver=None):
"""Try to solve linear program with given or default solver.

Expand All @@ -88,6 +95,8 @@ def lpsolve(c, G, h, solver=None):
result = _solve_lp_using_cvxopt(c, G, h, solver=solver)
elif solver == 'scipy':
result = _solve_lp_using_scipy(c, G, h)
elif solver == 'gurobi':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing the selection 'gurobi' as a keyword argument as done here is explicit [PEP 8] and seems preferable.

result = _solve_lp_using_gurobi(c, G, h)
else:
raise Exception(
'unknown LP solver "{s}".'.format(s=solver))
Expand Down Expand Up @@ -145,6 +154,34 @@ def _solve_lp_using_scipy(c, G, h):
fun=sol.fun)


def _solve_lp_using_gurobi(c, G, h):
"""Attempt linear optimization using gurobipy"""
_assert_have_solver('gurobi')
m = gp.Model()
x = m.addMVar(G.shape[1], lb=-gp.GRB.INFINITY)
m.addConstr(G@x <= h)
m.setObjective(c@x)
m.optimize()

result = dict()
if m.Status == gp.GRB.OPTIMAL:
result['status'] = 0
result['x'] = x.x
result['fun'] = m.ObjVal
return result
elif m.Status == gp.GRB.INFEASIBLE:
result['status'] = 2
elif m.Status == gp.GRB.INF_OR_UNBD or m.Status == gp.GRB.UNBOUNDED:
result['status'] = 3
else:
raise ValueError(f'`gurobipy` returned unexpected status value: {m.Status}')

result['x'] = None
result['fun'] = None
return result



def _assert_have_solver(solver):
"""Raise `RuntimeError` if `solver` is absent."""
if solver in installed_solvers:
Expand Down
2 changes: 1 addition & 1 deletion requirements/extras.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
cvxopt==1.2.4
cvxopt==1.2.4
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line appears to have only whitespace changes. It seems that the line could be omitted from the changeset.

9 changes: 9 additions & 0 deletions tests/polytope_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,5 +486,14 @@ def is_glpk_present():
return False


def test_gurobipy_return_same_result_as_scipy():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is good to test the added functionality. However, the absence of the solver gurobi on Travis CI implies that this test fails. Skimming the licensing webpage of gurobi gives the impression that installing gurobi on Travis CI is probably far from its licensing procedure. So it seems that this test would need to not be invoked on Travis CI.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today I learned that Gurobi has a limited license that is included by default when installing gurobipy, so it is possible now to run tests with Gurobi in CI without requesting special permission1, assuming the model is not "too large" 2.

Footnotes

  1. https://support.gurobi.com/hc/en-us/articles/360044290292-How-do-I-install-Gurobi-for-Python#h_01GRC369WKBMV6VFRCGJPRTSW5

  2. https://support.gurobi.com/hc/en-us/articles/360051597492-How-do-I-resolve-a-Model-too-large-for-size-limited-Gurobi-license-error

c, A, b = example_1d()
result_gurobi = solvers.lpsolve(c, A, b, solver='gurobi')
result_scipy = solvers.lpsolve(c, A, b, solver='scipy')
nt.assert_equal(result_scipy['status'], result_gurobi['status'])
nt.assert_almost_equal(result_scipy['x'][0], result_gurobi['x'][0])
nt.assert_almost_equal(result_scipy['fun'], result_gurobi['fun'])


if __name__ == '__main__':
pass