Add first model feature (constraint RHS)

master
Alinson S. Xavier 5 years ago
parent 31ca45036a
commit 1397937f03

@ -0,0 +1,26 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
from typing import TYPE_CHECKING
from miplearn.types import ModelFeatures
if TYPE_CHECKING:
from miplearn import InternalSolver
class ModelFeaturesExtractor:
def __init__(
self,
internal_solver: "InternalSolver",
) -> None:
self.internal_solver = internal_solver
def extract(self) -> ModelFeatures:
rhs = {}
for cid in self.internal_solver.get_constraint_ids():
rhs[cid] = self.internal_solver.get_constraint_rhs(cid)
return {
"ConstraintRHS": rhs,
}

@ -9,7 +9,7 @@ from typing import Any, List, Optional, Hashable
import numpy as np import numpy as np
from miplearn.types import TrainingSample, VarIndex from miplearn.types import TrainingSample, VarIndex, ModelFeatures
class Instance(ABC): class Instance(ABC):
@ -24,8 +24,9 @@ class Instance(ABC):
features, which can be provided as inputs to machine learning models. features, which can be provided as inputs to machine learning models.
""" """
def __init__(self): def __init__(self) -> None:
self.training_data: List[TrainingSample] = [] self.training_data: List[TrainingSample] = []
self.model_features: ModelFeatures = {}
@abstractmethod @abstractmethod
def to_model(self) -> Any: def to_model(self) -> Any:

@ -1,6 +1,7 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from typing import List
import numpy as np import numpy as np
import pyomo.environ as pe import pyomo.environ as pe
@ -24,7 +25,6 @@ class ChallengeA:
n_training_instances=500, n_training_instances=500,
n_test_instances=50, n_test_instances=50,
): ):
np.random.seed(seed) np.random.seed(seed)
self.gen = MultiKnapsackGenerator( self.gen = MultiKnapsackGenerator(
n=randint(low=250, high=251), n=randint(low=250, high=251),
@ -241,7 +241,12 @@ class KnapsackInstance(Instance):
Simpler (one-dimensional) Knapsack Problem, used for testing. Simpler (one-dimensional) Knapsack Problem, used for testing.
""" """
def __init__(self, weights, prices, capacity): def __init__(
self,
weights: List[float],
prices: List[float],
capacity: float,
) -> None:
super().__init__() super().__init__()
self.weights = weights self.weights = weights
self.prices = prices self.prices = prices
@ -282,7 +287,12 @@ class GurobiKnapsackInstance(KnapsackInstance):
instead of Pyomo, used for testing. instead of Pyomo, used for testing.
""" """
def __init__(self, weights, prices, capacity): def __init__(
self,
weights: List[float],
prices: List[float],
capacity: float,
) -> None:
super().__init__(weights, prices, capacity) super().__init__(weights, prices, capacity)
def to_model(self): def to_model(self):

@ -335,6 +335,10 @@ class GurobiSolver(InternalSolver):
self.model.update() self.model.update()
return [c.ConstrName for c in self.model.getConstrs()] return [c.ConstrName for c in self.model.getConstrs()]
def get_constraint_rhs(self, cid: str) -> float:
assert self.model is not None
return self.model.getConstrByName(cid).rhs
def extract_constraint(self, cid): def extract_constraint(self, cid):
self._raise_if_callback() self._raise_if_callback()
constr = self.model.getConstrByName(cid) constr = self.model.getConstrByName(cid)

@ -155,6 +155,13 @@ class InternalSolver(ABC):
""" """
pass pass
@abstractmethod
def get_constraint_rhs(self, cid: str) -> float:
"""
Returns the right-hand side of a given constraint.
"""
pass
@abstractmethod @abstractmethod
def add_constraint(self, cobj: Constraint) -> None: def add_constraint(self, cobj: Constraint) -> None:
""" """

@ -16,11 +16,12 @@ from miplearn.components.cuts import UserCutsComponent
from miplearn.components.lazy_dynamic import DynamicLazyConstraintsComponent from miplearn.components.lazy_dynamic import DynamicLazyConstraintsComponent
from miplearn.components.objective import ObjectiveValueComponent from miplearn.components.objective import ObjectiveValueComponent
from miplearn.components.primal import PrimalSolutionComponent from miplearn.components.primal import PrimalSolutionComponent
from miplearn.features import ModelFeaturesExtractor
from miplearn.instance import Instance from miplearn.instance import Instance
from miplearn.solvers import _RedirectOutput from miplearn.solvers import _RedirectOutput
from miplearn.solvers.internal import InternalSolver from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
from miplearn.types import MIPSolveStats, TrainingSample, LearningSolveStats from miplearn.types import TrainingSample, LearningSolveStats
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -164,6 +165,10 @@ class LearningSolver:
assert isinstance(self.internal_solver, InternalSolver) assert isinstance(self.internal_solver, InternalSolver)
self.internal_solver.set_instance(instance, model) self.internal_solver.set_instance(instance, model)
# Extract model features
extractor = ModelFeaturesExtractor(self.internal_solver)
instance.model_features = extractor.extract()
# Solve linear relaxation # Solve linear relaxation
if self.solve_lp_first: if self.solve_lp_first:
logger.info("Solving LP relaxation...") logger.info("Solving LP relaxation...")

@ -212,7 +212,11 @@ class BasePyomoSolver(InternalSolver):
assert self.model is not None assert self.model is not None
self._cname_to_constr = {} self._cname_to_constr = {}
for constr in self.model.component_objects(Constraint): for constr in self.model.component_objects(Constraint):
self._cname_to_constr[constr.name] = constr if isinstance(constr, pe.ConstraintList):
for idx in constr:
self._cname_to_constr[f"{constr.name}[{idx}]"] = constr[idx]
else:
self._cname_to_constr[constr.name] = constr
def fix(self, solution): def fix(self, solution):
count_total, count_fixed = 0, 0 count_total, count_fixed = 0, 0
@ -302,6 +306,13 @@ class BasePyomoSolver(InternalSolver):
else: else:
return "=" return "="
def get_constraint_rhs(self, cid: str) -> float:
cobj = self._cname_to_constr[cid]
if cobj.has_ub:
return cobj.upper()
else:
return cobj.lower()
def set_constraint_sense(self, cid: str, sense: str) -> None: def set_constraint_sense(self, cid: str, sense: str) -> None:
raise Exception("Not implemented") raise Exception("Not implemented")

@ -71,6 +71,14 @@ LearningSolveStats = TypedDict(
total=False, total=False,
) )
ModelFeatures = TypedDict(
"ModelFeatures",
{
"ConstraintRHS": Dict[str, float],
},
total=False,
)
IterationCallback = Callable[[], bool] IterationCallback = Callable[[], bool]
LazyCallback = Callable[[Any, Any], None] LazyCallback = Callable[[Any, Any], None]

@ -1,26 +1,3 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from miplearn.problems.knapsack import KnapsackInstance
from miplearn.solvers.learning import LearningSolver
def get_test_pyomo_instances():
instances = [
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

@ -12,7 +12,7 @@ from miplearn.classifiers import Classifier
from miplearn.components.lazy_dynamic import DynamicLazyConstraintsComponent from miplearn.components.lazy_dynamic import DynamicLazyConstraintsComponent
from miplearn.solvers.internal import InternalSolver from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.learning import LearningSolver from miplearn.solvers.learning import LearningSolver
from .. import get_test_pyomo_instances from tests.fixtures.knapsack import get_test_pyomo_instances
E = 0.1 E = 0.1

@ -1,6 +1,7 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from typing import cast from typing import cast
from unittest.mock import Mock from unittest.mock import Mock
@ -10,7 +11,7 @@ from numpy.testing import assert_array_equal
from miplearn.instance import Instance from miplearn.instance import Instance
from miplearn.classifiers import Regressor from miplearn.classifiers import Regressor
from miplearn.components.objective import ObjectiveValueComponent from miplearn.components.objective import ObjectiveValueComponent
from .. import get_test_pyomo_instances from tests.fixtures.knapsack import get_test_pyomo_instances
def test_x_y_predict() -> None: def test_x_y_predict() -> None:

@ -11,7 +11,6 @@ from miplearn import Classifier
from miplearn.classifiers.threshold import Threshold, MinPrecisionThreshold from miplearn.classifiers.threshold import Threshold, MinPrecisionThreshold
from miplearn.components.primal import PrimalSolutionComponent from miplearn.components.primal import PrimalSolutionComponent
from miplearn.instance import Instance from miplearn.instance import Instance
from tests import get_test_pyomo_instances
def test_x_y_fit() -> None: def test_x_y_fit() -> None:

@ -0,0 +1,45 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
from miplearn import BasePyomoSolver, GurobiSolver, InternalSolver, Instance
from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance
from miplearn.solvers.learning import LearningSolver
from tests.solvers import _is_subclass_or_instance
def get_test_pyomo_instances():
instances = [
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

@ -3,7 +3,7 @@
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from inspect import isclass from inspect import isclass
from typing import List, Callable from typing import List, Callable, Any
from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance
from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.gurobi import GurobiSolver
@ -13,7 +13,7 @@ from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
from miplearn.solvers.pyomo.xpress import XpressPyomoSolver from miplearn.solvers.pyomo.xpress import XpressPyomoSolver
def _is_subclass_or_instance(obj, parent_class): def _is_subclass_or_instance(obj: Any, parent_class: Any) -> bool:
return isinstance(obj, parent_class) or ( return isinstance(obj, parent_class) or (
isclass(obj) and issubclass(obj, parent_class) isclass(obj) and issubclass(obj, parent_class)
) )
@ -35,5 +35,5 @@ def _get_knapsack_instance(solver):
assert False assert False
def _get_internal_solvers() -> List[Callable[[], InternalSolver]]: def get_internal_solvers() -> List[Callable[[], InternalSolver]]:
return [GurobiPyomoSolver, GurobiSolver, XpressPyomoSolver] return [GurobiPyomoSolver, GurobiSolver, XpressPyomoSolver]

@ -13,7 +13,7 @@ from miplearn.solvers.gurobi import GurobiSolver
from miplearn.solvers.pyomo.base import BasePyomoSolver from miplearn.solvers.pyomo.base import BasePyomoSolver
from . import ( from . import (
_get_knapsack_instance, _get_knapsack_instance,
_get_internal_solvers, get_internal_solvers,
) )
from ..fixtures.infeasible import get_infeasible_instance from ..fixtures.infeasible import get_infeasible_instance
@ -32,7 +32,7 @@ def test_redirect_output():
def test_internal_solver_warm_starts(): def test_internal_solver_warm_starts():
for solver_class in _get_internal_solvers(): for solver_class in get_internal_solvers():
logger.info("Solver: %s" % solver_class) logger.info("Solver: %s" % solver_class)
instance = _get_knapsack_instance(solver_class) instance = _get_knapsack_instance(solver_class)
model = instance.to_model() model = instance.to_model()
@ -83,7 +83,7 @@ def test_internal_solver_warm_starts():
def test_internal_solver(): def test_internal_solver():
for solver_class in _get_internal_solvers(): for solver_class in get_internal_solvers():
logger.info("Solver: %s" % solver_class) logger.info("Solver: %s" % solver_class)
instance = _get_knapsack_instance(solver_class) instance = _get_knapsack_instance(solver_class)
@ -175,7 +175,7 @@ def test_internal_solver():
def test_relax(): def test_relax():
for solver_class in _get_internal_solvers(): for solver_class in get_internal_solvers():
instance = _get_knapsack_instance(solver_class) instance = _get_knapsack_instance(solver_class)
solver = solver_class() solver = solver_class()
solver.set_instance(instance) solver.set_instance(instance)
@ -185,7 +185,7 @@ def test_relax():
def test_infeasible_instance(): def test_infeasible_instance():
for solver_class in _get_internal_solvers(): for solver_class in get_internal_solvers():
instance = get_infeasible_instance(solver_class) instance = get_infeasible_instance(solver_class)
solver = solver_class() solver = solver_class()
solver.set_instance(instance) solver.set_instance(instance)
@ -203,7 +203,7 @@ def test_infeasible_instance():
def test_iteration_cb(): def test_iteration_cb():
for solver_class in _get_internal_solvers(): for solver_class in get_internal_solvers():
logger.info("Solver: %s" % solver_class) logger.info("Solver: %s" % solver_class)
instance = _get_knapsack_instance(solver_class) instance = _get_knapsack_instance(solver_class)
solver = solver_class() solver = solver_class()

@ -10,14 +10,14 @@ import os
from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.gurobi import GurobiSolver
from miplearn.solvers.learning import LearningSolver from miplearn.solvers.learning import LearningSolver
from . import _get_knapsack_instance, _get_internal_solvers from . import _get_knapsack_instance, get_internal_solvers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def test_learning_solver(): def test_learning_solver():
for mode in ["exact", "heuristic"]: for mode in ["exact", "heuristic"]:
for internal_solver in _get_internal_solvers(): for internal_solver in get_internal_solvers():
logger.info("Solver: %s" % internal_solver) logger.info("Solver: %s" % internal_solver)
instance = _get_knapsack_instance(internal_solver) instance = _get_knapsack_instance(internal_solver)
solver = LearningSolver( solver = LearningSolver(
@ -26,6 +26,9 @@ def test_learning_solver():
) )
solver.solve(instance) solver.solve(instance)
assert hasattr(instance, "model_features")
data = instance.training_data[0] data = instance.training_data[0]
assert data["Solution"]["x"][0] == 1.0 assert data["Solution"]["x"][0] == 1.0
assert data["Solution"]["x"][1] == 0.0 assert data["Solution"]["x"][1] == 0.0
@ -49,7 +52,7 @@ def test_learning_solver():
def test_solve_without_lp(): def test_solve_without_lp():
for internal_solver in _get_internal_solvers(): for internal_solver in get_internal_solvers():
logger.info("Solver: %s" % internal_solver) logger.info("Solver: %s" % internal_solver)
instance = _get_knapsack_instance(internal_solver) instance = _get_knapsack_instance(internal_solver)
solver = LearningSolver( solver = LearningSolver(
@ -62,7 +65,7 @@ def test_solve_without_lp():
def test_parallel_solve(): def test_parallel_solve():
for internal_solver in _get_internal_solvers(): for internal_solver in get_internal_solvers():
instances = [_get_knapsack_instance(internal_solver) for _ in range(10)] instances = [_get_knapsack_instance(internal_solver) for _ in range(10)]
solver = LearningSolver(solver=internal_solver) solver = LearningSolver(solver=internal_solver)
results = solver.parallel_solve(instances, n_jobs=3) results = solver.parallel_solve(instances, n_jobs=3)
@ -73,7 +76,7 @@ def test_parallel_solve():
def test_solve_fit_from_disk(): def test_solve_fit_from_disk():
for internal_solver in _get_internal_solvers(): for internal_solver in get_internal_solvers():
# Create instances and pickle them # Create instances and pickle them
filenames = [] filenames = []
for k in range(3): for k in range(3):

@ -0,0 +1,23 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
from miplearn.features import ModelFeaturesExtractor
from tests.fixtures.knapsack import get_knapsack_instance
from tests.solvers import get_internal_solvers
def test_knapsack() -> None:
for solver_factory in get_internal_solvers():
# Initialize model, instance and internal solver
solver = solver_factory()
instance = get_knapsack_instance(solver)
model = instance.to_model()
solver.set_instance(instance, model)
# Extract all model features
extractor = ModelFeaturesExtractor(solver)
features = extractor.extract()
# Test constraint features
print(solver, features)
assert features["ConstraintRHS"]["eq_capacity"] == 67.0
Loading…
Cancel
Save