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

@ -1,6 +1,7 @@
# 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 List
import numpy as np
import pyomo.environ as pe
@ -24,7 +25,6 @@ class ChallengeA:
n_training_instances=500,
n_test_instances=50,
):
np.random.seed(seed)
self.gen = MultiKnapsackGenerator(
n=randint(low=250, high=251),
@ -241,7 +241,12 @@ class KnapsackInstance(Instance):
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__()
self.weights = weights
self.prices = prices
@ -282,7 +287,12 @@ class GurobiKnapsackInstance(KnapsackInstance):
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)
def to_model(self):

@ -335,6 +335,10 @@ class GurobiSolver(InternalSolver):
self.model.update()
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):
self._raise_if_callback()
constr = self.model.getConstrByName(cid)

@ -155,6 +155,13 @@ class InternalSolver(ABC):
"""
pass
@abstractmethod
def get_constraint_rhs(self, cid: str) -> float:
"""
Returns the right-hand side of a given constraint.
"""
pass
@abstractmethod
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.objective import ObjectiveValueComponent
from miplearn.components.primal import PrimalSolutionComponent
from miplearn.features import ModelFeaturesExtractor
from miplearn.instance import Instance
from miplearn.solvers import _RedirectOutput
from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
from miplearn.types import MIPSolveStats, TrainingSample, LearningSolveStats
from miplearn.types import TrainingSample, LearningSolveStats
logger = logging.getLogger(__name__)
@ -164,6 +165,10 @@ class LearningSolver:
assert isinstance(self.internal_solver, InternalSolver)
self.internal_solver.set_instance(instance, model)
# Extract model features
extractor = ModelFeaturesExtractor(self.internal_solver)
instance.model_features = extractor.extract()
# Solve linear relaxation
if self.solve_lp_first:
logger.info("Solving LP relaxation...")

@ -212,6 +212,10 @@ class BasePyomoSolver(InternalSolver):
assert self.model is not None
self._cname_to_constr = {}
for constr in self.model.component_objects(Constraint):
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):
@ -302,6 +306,13 @@ class BasePyomoSolver(InternalSolver):
else:
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:
raise Exception("Not implemented")

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

@ -1,26 +1,3 @@
# 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.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.solvers.internal import InternalSolver
from miplearn.solvers.learning import LearningSolver
from .. import get_test_pyomo_instances
from tests.fixtures.knapsack import get_test_pyomo_instances
E = 0.1

@ -1,6 +1,7 @@
# 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 cast
from unittest.mock import Mock
@ -10,7 +11,7 @@ from numpy.testing import assert_array_equal
from miplearn.instance import Instance
from miplearn.classifiers import Regressor
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:

@ -11,7 +11,6 @@ from miplearn import Classifier
from miplearn.classifiers.threshold import Threshold, MinPrecisionThreshold
from miplearn.components.primal import PrimalSolutionComponent
from miplearn.instance import Instance
from tests import get_test_pyomo_instances
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.
from inspect import isclass
from typing import List, Callable
from typing import List, Callable, Any
from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance
from miplearn.solvers.gurobi import GurobiSolver
@ -13,7 +13,7 @@ from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
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 (
isclass(obj) and issubclass(obj, parent_class)
)
@ -35,5 +35,5 @@ def _get_knapsack_instance(solver):
assert False
def _get_internal_solvers() -> List[Callable[[], InternalSolver]]:
def get_internal_solvers() -> List[Callable[[], InternalSolver]]:
return [GurobiPyomoSolver, GurobiSolver, XpressPyomoSolver]

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

@ -10,14 +10,14 @@ import os
from miplearn.solvers.gurobi import GurobiSolver
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__)
def test_learning_solver():
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)
instance = _get_knapsack_instance(internal_solver)
solver = LearningSolver(
@ -26,6 +26,9 @@ def test_learning_solver():
)
solver.solve(instance)
assert hasattr(instance, "model_features")
data = instance.training_data[0]
assert data["Solution"]["x"][0] == 1.0
assert data["Solution"]["x"][1] == 0.0
@ -49,7 +52,7 @@ def test_learning_solver():
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)
instance = _get_knapsack_instance(internal_solver)
solver = LearningSolver(
@ -62,7 +65,7 @@ def test_solve_without_lp():
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)]
solver = LearningSolver(solver=internal_solver)
results = solver.parallel_solve(instances, n_jobs=3)
@ -73,7 +76,7 @@ def test_parallel_solve():
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
filenames = []
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