From a8224b5a38299e0e8f80f7e55d4dabd5e9f77ed5 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Fri, 9 Apr 2021 18:45:06 -0500 Subject: [PATCH] Move instance fixtures into the main source; remove duplication --- miplearn/problems/knapsack.py | 32 ---------- miplearn/solvers/gurobi.py | 75 +++++++++++++++++++++++ miplearn/solvers/internal.py | 12 ++++ miplearn/solvers/pyomo/base.py | 87 +++++++++++++++++++++++++++ tests/components/test_objective.py | 3 +- tests/fixtures/__init__.py | 3 - tests/fixtures/infeasible.py | 44 -------------- tests/fixtures/knapsack.py | 50 --------------- tests/fixtures/redundant.py | 42 ------------- tests/instance/test_picklegz.py | 3 +- tests/solvers/__init__.py | 33 ---------- tests/solvers/test_internal_solver.py | 12 ++-- tests/solvers/test_lazy_cb.py | 3 +- tests/solvers/test_learning_solver.py | 11 ++-- tests/test_features.py | 3 +- 15 files changed, 188 insertions(+), 225 deletions(-) delete mode 100644 tests/fixtures/__init__.py delete mode 100644 tests/fixtures/infeasible.py delete mode 100644 tests/fixtures/knapsack.py delete mode 100644 tests/fixtures/redundant.py diff --git a/miplearn/problems/knapsack.py b/miplearn/problems/knapsack.py index b03f2bc..57faf43 100644 --- a/miplearn/problems/knapsack.py +++ b/miplearn/problems/knapsack.py @@ -289,35 +289,3 @@ class KnapsackInstance(Instance): self.weights[item], self.prices[item], ] - - -class GurobiKnapsackInstance(KnapsackInstance): - """ - Simpler (one-dimensional) knapsack instance, implemented directly in Gurobi - instead of Pyomo, used for testing. - """ - - def __init__( - self, - weights: List[float], - prices: List[float], - capacity: float, - ) -> None: - super().__init__(weights, prices, capacity) - - @overrides - def to_model(self) -> Any: - import gurobipy as gp - from gurobipy import GRB - - model = gp.Model("Knapsack") - n = len(self.weights) - x = model.addVars(n, vtype=GRB.BINARY, name="x") - model.addConstr( - gp.quicksum(x[i] * self.weights[i] for i in range(n)) <= self.capacity, - "eq_capacity", - ) - model.setObjective( - gp.quicksum(x[i] * self.prices[i] for i in range(n)), GRB.MAXIMIZE - ) - return model diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index ffd744d..0431efc 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -19,6 +19,7 @@ from miplearn.solvers.internal import ( LazyCallback, MIPSolveStats, ) +from miplearn.solvers.pyomo.base import PyomoTestInstanceKnapsack from miplearn.types import ( SolverParams, UserCutCallback, @@ -442,3 +443,77 @@ class GurobiSolver(InternalSolver): params=self.params, lazy_cb_frequency=self.lazy_cb_frequency, ) + + @overrides + def build_test_instance_infeasible(self) -> Instance: + return GurobiTestInstanceInfeasible() + + @overrides + def build_test_instance_redundancy(self) -> Instance: + return GurobiTestInstanceRedundancy() + + @overrides + def build_test_instance_knapsack(self) -> Instance: + return GurobiTestInstanceKnapsack( + weights=[23.0, 26.0, 20.0, 18.0], + prices=[505.0, 352.0, 458.0, 220.0], + capacity=67.0, + ) + + +class GurobiTestInstanceInfeasible(Instance): + @overrides + def to_model(self) -> Any: + import gurobipy as gp + from gurobipy import GRB + + model = gp.Model() + x = model.addVars(1, vtype=GRB.BINARY, name="x") + model.addConstr(x[0] >= 2) + model.setObjective(x[0]) + return model + + +class GurobiTestInstanceRedundancy(Instance): + def to_model(self) -> Any: + import gurobipy as gp + from gurobipy import GRB + + model = gp.Model() + x = model.addVars(2, vtype=GRB.BINARY, name="x") + model.addConstr(x[0] + x[1] <= 1) + model.addConstr(x[0] + x[1] <= 2) + model.setObjective(x[0] + x[1], GRB.MAXIMIZE) + return model + + +class GurobiTestInstanceKnapsack(PyomoTestInstanceKnapsack): + """ + Simpler (one-dimensional) knapsack instance, implemented directly in Gurobi + instead of Pyomo, used for testing. + """ + + def __init__( + self, + weights: List[float], + prices: List[float], + capacity: float, + ) -> None: + super().__init__(weights, prices, capacity) + + @overrides + def to_model(self) -> Any: + import gurobipy as gp + from gurobipy import GRB + + model = gp.Model("Knapsack") + n = len(self.weights) + x = model.addVars(n, vtype=GRB.BINARY, name="x") + model.addConstr( + gp.quicksum(x[i] * self.weights[i] for i in range(n)) <= self.capacity, + "eq_capacity", + ) + model.setObjective( + gp.quicksum(x[i] * self.prices[i] for i in range(n)), GRB.MAXIMIZE + ) + return model diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index 067b1e6..6a9bb5d 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -292,3 +292,15 @@ class InternalSolver(ABC): completely unitialized. """ pass + + @abstractmethod + def build_test_instance_infeasible(self) -> Instance: + pass + + @abstractmethod + def build_test_instance_redundancy(self) -> Instance: + pass + + @abstractmethod + def build_test_instance_knapsack(self) -> Instance: + pass diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index 8fb1e48..c8af6e4 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -29,7 +29,9 @@ from miplearn.types import ( UserCutCallback, Solution, VariableName, + Category, ) +import numpy as np logger = logging.getLogger(__name__) @@ -338,3 +340,88 @@ class BasePyomoSolver(InternalSolver): @overrides def get_sense(self) -> str: return self._obj_sense + + @overrides + def build_test_instance_infeasible(self) -> Instance: + return PyomoTestInstanceInfeasible() + + @overrides + def build_test_instance_redundancy(self) -> Instance: + return PyomoTestInstanceRedundancy() + + @overrides + def build_test_instance_knapsack(self) -> Instance: + return PyomoTestInstanceKnapsack( + weights=[23.0, 26.0, 20.0, 18.0], + prices=[505.0, 352.0, 458.0, 220.0], + capacity=67.0, + ) + + +class PyomoTestInstanceInfeasible(Instance): + @overrides + def to_model(self) -> pe.ConcreteModel: + model = pe.ConcreteModel() + model.x = pe.Var([0], domain=pe.Binary) + model.OBJ = pe.Objective(expr=model.x[0], sense=pe.maximize) + model.eq = pe.Constraint(expr=model.x[0] >= 2) + return model + + +class PyomoTestInstanceRedundancy(Instance): + def to_model(self) -> pe.ConcreteModel: + model = pe.ConcreteModel() + model.x = pe.Var([0, 1], domain=pe.Binary) + model.OBJ = pe.Objective(expr=model.x[0] + model.x[1], sense=pe.maximize) + model.eq1 = pe.Constraint(expr=model.x[0] + model.x[1] <= 1) + model.eq2 = pe.Constraint(expr=model.x[0] + model.x[1] <= 2) + return model + + +class PyomoTestInstanceKnapsack(Instance): + """ + Simpler (one-dimensional) Knapsack Problem, used for testing. + """ + + def __init__( + self, + weights: List[float], + prices: List[float], + capacity: float, + ) -> None: + super().__init__() + self.weights = weights + self.prices = prices + self.capacity = capacity + self.varname_to_item: Dict[VariableName, int] = { + f"x[{i}]": i for i in range(len(self.weights)) + } + + @overrides + def to_model(self) -> pe.ConcreteModel: + model = pe.ConcreteModel() + items = range(len(self.weights)) + model.x = pe.Var(items, domain=pe.Binary) + model.OBJ = pe.Objective( + expr=sum(model.x[v] * self.prices[v] for v in items), + sense=pe.maximize, + ) + model.eq_capacity = pe.Constraint( + expr=sum(model.x[v] * self.weights[v] for v in items) <= self.capacity + ) + return model + + @overrides + def get_instance_features(self) -> List[float]: + return [ + self.capacity, + np.average(self.weights), + ] + + @overrides + def get_variable_features(self, var_name: VariableName) -> List[Category]: + item = self.varname_to_item[var_name] + return [ + self.weights[item], + self.prices[item], + ] diff --git a/tests/components/test_objective.py b/tests/components/test_objective.py index b5f3ce2..0f6a322 100644 --- a/tests/components/test_objective.py +++ b/tests/components/test_objective.py @@ -14,7 +14,6 @@ from miplearn.features import TrainingSample, InstanceFeatures, Features from miplearn.instance.base import Instance from miplearn.solvers.learning import LearningSolver from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver -from tests.fixtures.knapsack import get_knapsack_instance @pytest.fixture @@ -252,7 +251,7 @@ def test_sample_evaluate(instance: Instance, sample: TrainingSample) -> None: def test_usage() -> None: solver = LearningSolver(components=[ObjectiveValueComponent()]) - instance = get_knapsack_instance(GurobiPyomoSolver()) + instance = GurobiPyomoSolver().build_test_instance_knapsack() solver.solve(instance) solver.fit([instance]) stats = solver.solve(instance) diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py deleted file mode 100644 index 5fbccb1..0000000 --- a/tests/fixtures/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization -# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. -# Released under the modified BSD license. See COPYING.md for more details. diff --git a/tests/fixtures/infeasible.py b/tests/fixtures/infeasible.py deleted file mode 100644 index 77b5037..0000000 --- a/tests/fixtures/infeasible.py +++ /dev/null @@ -1,44 +0,0 @@ -# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization -# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. -# Released under the modified BSD license. See COPYING.md for more details. - -from typing import Any - -from overrides import overrides -from pyomo import environ as pe - -from miplearn.instance.base import Instance -from miplearn.solvers.gurobi import GurobiSolver -from miplearn.solvers.pyomo.base import BasePyomoSolver -from tests.solvers import _is_subclass_or_instance - - -class InfeasiblePyomoInstance(Instance): - @overrides - def to_model(self) -> pe.ConcreteModel: - model = pe.ConcreteModel() - model.x = pe.Var([0], domain=pe.Binary) - model.OBJ = pe.Objective(expr=model.x[0], sense=pe.maximize) - model.eq = pe.Constraint(expr=model.x[0] >= 2) - return model - - -class InfeasibleGurobiInstance(Instance): - @overrides - def to_model(self) -> Any: - import gurobipy as gp - from gurobipy import GRB - - model = gp.Model() - x = model.addVars(1, vtype=GRB.BINARY, name="x") - model.addConstr(x[0] >= 2) - model.setObjective(x[0]) - return model - - -def get_infeasible_instance(solver: Any) -> Instance: - if _is_subclass_or_instance(solver, BasePyomoSolver): - return InfeasiblePyomoInstance() - if _is_subclass_or_instance(solver, GurobiSolver): - return InfeasibleGurobiInstance() - assert False diff --git a/tests/fixtures/knapsack.py b/tests/fixtures/knapsack.py deleted file mode 100644 index e7a8682..0000000 --- a/tests/fixtures/knapsack.py +++ /dev/null @@ -1,50 +0,0 @@ -# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization -# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. -# Released under the modified BSD license. See COPYING.md for more details. - -from typing import List, Any, Tuple - -from miplearn.instance.base import Instance -from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance -from miplearn.solvers.gurobi import GurobiSolver -from miplearn.solvers.internal import InternalSolver -from miplearn.solvers.learning import LearningSolver -from miplearn.solvers.pyomo.base import BasePyomoSolver -from tests.solvers import _is_subclass_or_instance - - -def get_test_pyomo_instances() -> Tuple[List[Instance], List[Any]]: - instances: List[Instance] = [ - KnapsackInstance( - weights=[23.0, 26.0, 20.0, 18.0], - prices=[505.0, 352.0, 458.0, 220.0], - capacity=67.0, - ), - KnapsackInstance( - weights=[25.0, 30.0, 22.0, 18.0], - prices=[500.0, 365.0, 420.0, 150.0], - capacity=70.0, - ), - ] - models = [instance.to_model() for instance in instances] - solver = LearningSolver() - for i in range(len(instances)): - solver.solve(instances[i], models[i]) - return instances, models - - -def get_knapsack_instance(solver: InternalSolver) -> Instance: - if _is_subclass_or_instance(solver, BasePyomoSolver): - return KnapsackInstance( - weights=[23.0, 26.0, 20.0, 18.0], - prices=[505.0, 352.0, 458.0, 220.0], - capacity=67.0, - ) - elif _is_subclass_or_instance(solver, GurobiSolver): - return GurobiKnapsackInstance( - weights=[23.0, 26.0, 20.0, 18.0], - prices=[505.0, 352.0, 458.0, 220.0], - capacity=67.0, - ) - else: - assert False diff --git a/tests/fixtures/redundant.py b/tests/fixtures/redundant.py deleted file mode 100644 index 27f6d96..0000000 --- a/tests/fixtures/redundant.py +++ /dev/null @@ -1,42 +0,0 @@ -# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization -# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. -# Released under the modified BSD license. See COPYING.md for more details. -from typing import Any - -import pyomo.environ as pe - -from miplearn.instance.base import Instance -from miplearn.solvers.gurobi import GurobiSolver -from miplearn.solvers.pyomo.base import BasePyomoSolver -from tests.solvers import _is_subclass_or_instance - - -class PyomoInstanceWithRedundancy(Instance): - def to_model(self) -> pe.ConcreteModel: - model = pe.ConcreteModel() - model.x = pe.Var([0, 1], domain=pe.Binary) - model.OBJ = pe.Objective(expr=model.x[0] + model.x[1], sense=pe.maximize) - model.eq1 = pe.Constraint(expr=model.x[0] + model.x[1] <= 1) - model.eq2 = pe.Constraint(expr=model.x[0] + model.x[1] <= 2) - return model - - -class GurobiInstanceWithRedundancy(Instance): - def to_model(self) -> Any: - import gurobipy as gp - from gurobipy import GRB - - model = gp.Model() - x = model.addVars(2, vtype=GRB.BINARY, name="x") - model.addConstr(x[0] + x[1] <= 1) - model.addConstr(x[0] + x[1] <= 2) - model.setObjective(x[0] + x[1], GRB.MAXIMIZE) - return model - - -def get_instance_with_redundancy(solver: Any) -> Instance: - if _is_subclass_or_instance(solver, BasePyomoSolver): - return PyomoInstanceWithRedundancy() - if _is_subclass_or_instance(solver, GurobiSolver): - return GurobiInstanceWithRedundancy() - assert False diff --git a/tests/instance/test_picklegz.py b/tests/instance/test_picklegz.py index 11b8ed6..ebdb017 100644 --- a/tests/instance/test_picklegz.py +++ b/tests/instance/test_picklegz.py @@ -5,11 +5,10 @@ import tempfile from miplearn.instance.picklegz import write_pickle_gz, PickleGzInstance from miplearn.solvers.gurobi import GurobiSolver -from tests.fixtures.knapsack import get_knapsack_instance def test_usage() -> None: - original = get_knapsack_instance(GurobiSolver()) + original = GurobiSolver().build_test_instance_knapsack() file = tempfile.NamedTemporaryFile() write_pickle_gz(original, file.name) pickled = PickleGzInstance(file.name) diff --git a/tests/solvers/__init__.py b/tests/solvers/__init__.py index 4cbc8b4..e69de29 100644 --- a/tests/solvers/__init__.py +++ b/tests/solvers/__init__.py @@ -1,33 +0,0 @@ -# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization -# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. -# Released under the modified BSD license. See COPYING.md for more details. - -from inspect import isclass -from typing import Any - -from miplearn.instance.base import Instance -from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance -from miplearn.solvers.gurobi import GurobiSolver -from miplearn.solvers.pyomo.base import BasePyomoSolver - - -def _is_subclass_or_instance(obj: Any, parent_class: Any) -> bool: - return isinstance(obj, parent_class) or ( - isclass(obj) and issubclass(obj, parent_class) - ) - - -def _get_knapsack_instance(solver: Any) -> Instance: - if _is_subclass_or_instance(solver, BasePyomoSolver): - return KnapsackInstance( - weights=[23.0, 26.0, 20.0, 18.0], - prices=[505.0, 352.0, 458.0, 220.0], - capacity=67.0, - ) - if _is_subclass_or_instance(solver, GurobiSolver): - return GurobiKnapsackInstance( - weights=[23.0, 26.0, 20.0, 18.0], - prices=[505.0, 352.0, 458.0, 220.0], - capacity=67.0, - ) - assert False diff --git a/tests/solvers/test_internal_solver.py b/tests/solvers/test_internal_solver.py index 75a8521..baed74f 100644 --- a/tests/solvers/test_internal_solver.py +++ b/tests/solvers/test_internal_solver.py @@ -13,11 +13,9 @@ from miplearn import InternalSolver from miplearn.solvers import _RedirectOutput from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.pyomo.base import BasePyomoSolver -from . import _get_knapsack_instance # noinspection PyUnresolvedReferences from .. import internal_solvers -from ..fixtures.infeasible import get_infeasible_instance logger = logging.getLogger(__name__) @@ -38,7 +36,7 @@ def test_internal_solver_warm_starts( ) -> None: for solver in internal_solvers: logger.info("Solver: %s" % solver) - instance = _get_knapsack_instance(solver) + instance = solver.build_test_instance_knapsack() model = instance.to_model() solver.set_instance(instance, model) solver.set_warm_start({"x[0]": 1.0, "x[1]": 0.0, "x[2]": 0.0, "x[3]": 1.0}) @@ -64,7 +62,7 @@ def test_internal_solver( for solver in internal_solvers: logger.info("Solver: %s" % solver) - instance = _get_knapsack_instance(solver) + instance = solver.build_test_instance_knapsack() model = instance.to_model() solver.set_instance(instance, model) @@ -169,7 +167,7 @@ def test_relax( internal_solvers: List[InternalSolver], ) -> None: for solver in internal_solvers: - instance = _get_knapsack_instance(solver) + instance = solver.build_test_instance_knapsack() solver.set_instance(instance) solver.relax() stats = solver.solve() @@ -181,7 +179,7 @@ def test_infeasible_instance( internal_solvers: List[InternalSolver], ) -> None: for solver in internal_solvers: - instance = get_infeasible_instance(solver) + instance = solver.build_test_instance_infeasible() solver.set_instance(instance) mip_stats = solver.solve() @@ -200,7 +198,7 @@ def test_iteration_cb( ) -> None: for solver in internal_solvers: logger.info("Solver: %s" % solver) - instance = _get_knapsack_instance(solver) + instance = solver.build_test_instance_knapsack() solver.set_instance(instance) count = 0 diff --git a/tests/solvers/test_lazy_cb.py b/tests/solvers/test_lazy_cb.py index fcf3e8f..1bd38d5 100644 --- a/tests/solvers/test_lazy_cb.py +++ b/tests/solvers/test_lazy_cb.py @@ -7,14 +7,13 @@ from typing import Any from miplearn import InternalSolver from miplearn.solvers.gurobi import GurobiSolver -from . import _get_knapsack_instance logger = logging.getLogger(__name__) def test_lazy_cb() -> None: solver = GurobiSolver() - instance = _get_knapsack_instance(solver) + instance = solver.build_test_instance_knapsack() model = instance.to_model() def lazy_cb(cb_solver: InternalSolver, cb_model: Any) -> None: diff --git a/tests/solvers/test_learning_solver.py b/tests/solvers/test_learning_solver.py index 8e6e633..d4f5cff 100644 --- a/tests/solvers/test_learning_solver.py +++ b/tests/solvers/test_learning_solver.py @@ -13,7 +13,6 @@ from miplearn import Instance, InternalSolver from miplearn.instance.picklegz import PickleGzInstance, write_pickle_gz, read_pickle_gz from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.learning import LearningSolver -from . import _get_knapsack_instance # noinspection PyUnresolvedReferences from tests import internal_solvers @@ -27,7 +26,7 @@ def test_learning_solver( for mode in ["exact", "heuristic"]: for internal_solver in internal_solvers: logger.info("Solver: %s" % internal_solver) - instance = _get_knapsack_instance(internal_solver) + instance = internal_solver.build_test_instance_knapsack() solver = LearningSolver( solver=internal_solver, mode=mode, @@ -71,7 +70,7 @@ def test_solve_without_lp( ) -> None: for internal_solver in internal_solvers: logger.info("Solver: %s" % internal_solver) - instance = _get_knapsack_instance(internal_solver) + instance = internal_solver.build_test_instance_knapsack() solver = LearningSolver( solver=internal_solver, solve_lp=False, @@ -85,7 +84,7 @@ def test_parallel_solve( internal_solvers: List[InternalSolver], ) -> None: for internal_solver in internal_solvers: - instances = [_get_knapsack_instance(internal_solver) for _ in range(10)] + instances = [internal_solver.build_test_instance_knapsack() for _ in range(10)] solver = LearningSolver(solver=internal_solver) results = solver.parallel_solve(instances, n_jobs=3) assert len(results) == 10 @@ -102,7 +101,7 @@ def test_solve_fit_from_disk( # Create instances and pickle them instances: List[Instance] = [] for k in range(3): - instance = _get_knapsack_instance(internal_solver) + instance = internal_solver.build_test_instance_knapsack() with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as file: instances += [PickleGzInstance(file.name)] write_pickle_gz(instance, file.name) @@ -132,7 +131,7 @@ def test_solve_fit_from_disk( def test_simulate_perfect() -> None: internal_solver = GurobiSolver() - instance = _get_knapsack_instance(internal_solver) + instance = internal_solver.build_test_instance_knapsack() with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as tmp: write_pickle_gz(instance, tmp.name) solver = LearningSolver( diff --git a/tests/test_features.py b/tests/test_features.py index dda2495..48c8d17 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -9,13 +9,12 @@ from miplearn.features import ( ConstraintFeatures, ) from miplearn.solvers.gurobi import GurobiSolver -from tests.fixtures.knapsack import get_knapsack_instance def test_knapsack() -> None: for solver_factory in [GurobiSolver]: solver = solver_factory() - instance = get_knapsack_instance(solver) + instance = solver.build_test_instance_knapsack() model = instance.to_model() solver.set_instance(instance, model) FeaturesExtractor(solver).extract(instance)