Skip to content

Commit 3dca6cb

Browse files
authored
Preference.Any uses SCIP if gurobi license is restricted. Add GurobiRestriced for explicit opt in to the restricted license (#75)
* fix: skip unlicensed gurobi * add comment * fix fallback * Improve Gurobi license check to honor all documented search locations and clarify license validation process * Enhance Gurobi test failure handling by adding specific marks for license modes * Update Gurobi preference handling and tests to use GurobiRestricted instead of GurobiUnlicensed * Refactor Preference class documentation for clarity and consistency
1 parent 0767246 commit 3dca6cb

2 files changed

Lines changed: 70 additions & 6 deletions

File tree

src/ilpy/solver_backends/__init__.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
11
from __future__ import annotations
22

3+
from contextlib import suppress
34
from enum import IntEnum, auto
5+
from functools import cache
46

57
from ._base import SolverBackend
68

79
__all__ = ["Preference", "SolverBackend", "create_solver_backend"]
810

911

1012
class Preference(IntEnum):
11-
"""Preference for a solver backend."""
13+
"""Preference for a solver backend.
14+
15+
A "full" Gurobi license means one that is *not* the size-limited license
16+
bundled with the `gurobipy` pip wheel. See
17+
https://support.gurobi.com/hc/en-us/articles/360051597492
18+
19+
- `Any`: Use Gurobi if a full license is available, otherwise fall back
20+
to SCIP. The bundled size-limited license is treated as "no license" to
21+
avoid silent "Model too large" failures on problems with >2000 variables.
22+
- `Scip`: Use SCIP. Raises if `pyscipopt` is not installed.
23+
- `Gurobi`: Use Gurobi; requires a full license. Raises otherwise. Use
24+
`GurobiRestricted` if you only have the bundled pip license.
25+
- `GurobiRestricted`: Use Gurobi with whatever license resolves
26+
(including the bundled size-limited pip license). Suitable for small
27+
problems (<2000 variables); larger ones will fail at solve time.
28+
"""
1229

1330
Any = auto()
1431
Scip = auto()
1532
Gurobi = auto()
33+
GurobiRestricted = auto()
1634

1735

1836
def create_solver_backend(preference: Preference | str) -> SolverBackend:
@@ -22,9 +40,14 @@ def create_solver_backend(preference: Preference | str) -> SolverBackend:
2240

2341
to_try = []
2442
if preference in (Preference.Any, Preference.Gurobi):
25-
to_try.append(("_gurobi", "GurobiSolver"))
43+
if _have_gurobi_license():
44+
to_try.append(("_gurobi", "GurobiSolver"))
45+
elif preference == Preference.Gurobi:
46+
raise RuntimeError("Gurobi license is not available. ")
2647
if preference in (Preference.Any, Preference.Scip):
2748
to_try.append(("_scip", "ScipSolver"))
49+
if preference in (Preference.Any, Preference.GurobiRestricted):
50+
to_try.append(("_gurobi", "GurobiSolver"))
2851

2952
errors: list[tuple[str, BaseException]] = []
3053
for modname, clsname in to_try:
@@ -42,3 +65,33 @@ def create_solver_backend(preference: Preference | str) -> SolverBackend:
4265
"Failed to create a solver backend. Tried:\n\n"
4366
+ "\n".join(f"- {name}:\n {e}" for name, e in errors)
4467
)
68+
69+
70+
@cache
71+
def _have_gurobi_license() -> bool:
72+
"""Return True if a real (non size-limited) Gurobi license is available.
73+
74+
Delegates license resolution to gurobipy itself so every documented search
75+
location is honored — the `GRB_LICENSE_FILE` environment variable, the
76+
user's home directory, and the platform-specific shared install directory
77+
(`/opt/gurobi` on Linux, `/Library/gurobi` on macOS, `C:\\gurobi` on
78+
Windows). See
79+
https://support.gurobi.com/hc/en-us/articles/360013417211
80+
81+
The pip/conda `gurobipy` wheel ships with a bundled size-limited license
82+
(max 2000 variables); it reports `LicenseID == 0`. We treat that case as
83+
"no license" so callers can fall back to SCIP for larger problems.
84+
"""
85+
try:
86+
import gurobipy as gp
87+
except ImportError:
88+
return False
89+
with suppress(Exception):
90+
# empty=True defers license validation until start()
91+
with gp.Env(empty=True) as env:
92+
# silence the startup banner and the "Restricted license" notice
93+
env.setParam("OutputFlag", 0)
94+
env.start()
95+
return int(env.getParam("LicenseID")) != 0
96+
# no license file found, or license expired
97+
return False

tests/test_solvers.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616

1717
from ilpy._constants import VariableType
1818

19-
# XFAIL if no gurobi not installed or no license found
20-
# (this is the best way I could find to determine this so far)
19+
# XFAIL gurobi tests if the requested license mode is unavailable:
20+
# - `gurobi` needs a full license
21+
# - `gurobi-restricted` needs at least the bundled/size-limited pip license
22+
# (which is poisoned if GRB_LICENSE_FILE is set but points to a stale file)
23+
from ilpy.solver_backends import create_solver_backend
24+
2125
gu_marks = []
2226
try:
23-
from ilpy.solver_backends import create_solver_backend
24-
2527
create_solver_backend(ilpy.Preference.Gurobi)
2628
import gurobipy as gb
2729

@@ -31,9 +33,18 @@
3133
gb = None
3234
HAVE_GUROBI = False
3335

36+
gr_marks = []
37+
try:
38+
create_solver_backend(ilpy.Preference.GurobiRestricted)
39+
except Exception as e:
40+
gr_marks.append(pytest.mark.xfail(reason=f"Gurobi restricted error: {e}"))
41+
3442
PREFS = [
3543
pytest.param(ilpy.Preference.Scip, id="scip"),
3644
pytest.param(ilpy.Preference.Gurobi, marks=gu_marks, id="gurobi"),
45+
pytest.param(
46+
ilpy.Preference.GurobiRestricted, marks=gr_marks, id="gurobi-restricted"
47+
),
3748
]
3849

3950

0 commit comments

Comments
 (0)