11from __future__ import annotations
22
3+ from contextlib import suppress
34from enum import IntEnum , auto
5+ from functools import cache
46
57from ._base import SolverBackend
68
79__all__ = ["Preference" , "SolverBackend" , "create_solver_backend" ]
810
911
1012class 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
1836def 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
0 commit comments