diff --git a/src/ilpy/_solver.py b/src/ilpy/_solver.py index 8cbd2a0..d2dad43 100644 --- a/src/ilpy/_solver.py +++ b/src/ilpy/_solver.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Callable from .expressions import Expression -from .solver_backends import Preference, SolverBackend, create_backend +from .solver_backends import Preference, SolverBackend, create_solver_backend if TYPE_CHECKING: from collections.abc import Iterator, Mapping, Sequence @@ -50,7 +50,7 @@ def __init__( preference: Preference = Preference.Any, ) -> None: vtpes: dict[int, VariableType] = dict(variable_types) if variable_types else {} - self._backend: SolverBackend = create_backend(preference) + self._backend: SolverBackend = create_solver_backend(preference) self._num_variables = num_variables self._backend.initialize(num_variables, default_variable_type, vtpes) diff --git a/src/ilpy/solver_backends/__init__.py b/src/ilpy/solver_backends/__init__.py index 6d86d3e..c4590c6 100644 --- a/src/ilpy/solver_backends/__init__.py +++ b/src/ilpy/solver_backends/__init__.py @@ -4,7 +4,7 @@ from ._base import SolverBackend -__all__ = ["Preference", "SolverBackend", "create_backend"] +__all__ = ["Preference", "SolverBackend", "create_solver_backend"] class Preference(IntEnum): @@ -15,20 +15,30 @@ class Preference(IntEnum): Gurobi = auto() -def create_backend(preference: Preference) -> SolverBackend: +def create_solver_backend(preference: Preference | str) -> SolverBackend: """Create a solver backend based on the preference.""" + if not isinstance(preference, Preference): + preference = Preference[str(preference).title()] + to_try = [] if preference in (Preference.Any, Preference.Gurobi): to_try.append(("_gurobi", "GurobiSolver")) - elif preference in (Preference.Any, Preference.Scip): + if preference in (Preference.Any, Preference.Scip): to_try.append(("_scip", "ScipSolver")) + errors: list[tuple[str, BaseException]] = [] for modname, clsname in to_try: + import_mod = f"ilpy.solver_backends.{modname}" try: - mod = __import__(f"ilpy.solver_backends.{modname}", fromlist=[clsname]) - except ImportError: - continue - else: - return getattr(mod, clsname)() # type: ignore [no-any-return] - - raise ValueError(f"Unknown preference: {preference}") # pragma: no cover + mod = __import__(import_mod, fromlist=[clsname]) + cls = getattr(mod, clsname) + backend = cls() + assert isinstance(backend, SolverBackend) + return backend + except Exception as e: # pragma: no cover + errors.append((f"{import_mod}::{clsname}", e)) + + raise RuntimeError( # pragma: no cover + "Failed to create a solver backend. Tried:\n\n" + + "\n".join(f"- {name}:\n {e}" for name, e in errors) + ) diff --git a/src/ilpy/solver_backends/_gurobi.py b/src/ilpy/solver_backends/_gurobi.py index 1dc257d..a6d89d4 100644 --- a/src/ilpy/solver_backends/_gurobi.py +++ b/src/ilpy/solver_backends/_gurobi.py @@ -47,16 +47,23 @@ class GurobiSolver(SolverBackend): + def __init__(self): + # we put this in __init__ instead of initialize so that it will raise an + # exception inside of create_backend if the module is imported but the + # license is not available + self._model = gb.Model() + def initialize( self, num_variables: int, default_variable_type: VariableType, variable_types: Mapping[int, VariableType], # TODO ) -> None: - self._model = model = gb.Model() # ilpy uses infinite bounds by default, but Gurobi uses 0 to infinity by default vtype = VTYPE_MAP[default_variable_type] - self._vars = model.addVars(num_variables, lb=-gb.GRB.INFINITY, vtype=vtype) + self._vars = self._model.addVars( + num_variables, lb=-gb.GRB.INFINITY, vtype=vtype + ) self._event_callback: Callable[[Mapping[str, float | str]], None] | None = None # 2 = non-convex quadratic problems are solved by means of translating them diff --git a/tests/test_solvers.py b/tests/test_solvers.py index efb45e7..20a901d 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -20,9 +20,11 @@ # (this is the best way I could find to determine this so far) gu_marks = [] try: + from ilpy.solver_backends import create_solver_backend + + create_solver_backend(ilpy.Preference.Gurobi) import gurobipy as gb - ilpy.Solver(0, ilpy.VariableType.Binary, None, ilpy.Preference.Gurobi) HAVE_GUROBI = True except Exception as e: gu_marks.append(pytest.mark.xfail(reason=f"Gurobi error: {e}"))