Replace build_lazy_constraint by enforce_lazy_constraint

master
Alinson S. Xavier 5 years ago
parent 735884151d
commit f70363db0d
No known key found for this signature in database
GPG Key ID: DCA0DAD4D2F58624

@ -51,8 +51,7 @@ class DynamicLazyConstraintsComponent(Component):
) -> None: ) -> None:
assert solver.internal_solver is not None assert solver.internal_solver is not None
for cid in cids: for cid in cids:
cobj = instance.build_lazy_constraint(model, cid) instance.enforce_lazy_constraint(solver.internal_solver, model, cid)
solver.internal_solver.add_constraint(cobj)
@overrides @overrides
def before_solve_mip( def before_solve_mip(
@ -78,7 +77,7 @@ class DynamicLazyConstraintsComponent(Component):
model: Any, model: Any,
) -> bool: ) -> bool:
logger.debug("Finding violated lazy constraints...") logger.debug("Finding violated lazy constraints...")
cids = instance.find_violated_lazy_constraints(model) cids = instance.find_violated_lazy_constraints(solver.internal_solver, model)
if len(cids) == 0: if len(cids) == 0:
logger.debug("No violations found") logger.debug("No violations found")
return False return False

@ -4,7 +4,7 @@
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, List, Optional, Hashable from typing import Any, List, Optional, Hashable, TYPE_CHECKING
from overrides import EnforceOverrides from overrides import EnforceOverrides
@ -13,9 +13,11 @@ from miplearn.types import VariableName, Category
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from miplearn.solvers.learning import InternalSolver
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
class Instance(ABC): class Instance(ABC, EnforceOverrides):
""" """
Abstract class holding all the data necessary to generate a concrete model of the Abstract class holding all the data necessary to generate a concrete model of the
proble. proble.
@ -109,28 +111,40 @@ class Instance(ABC):
def is_constraint_lazy(self, cid: str) -> bool: def is_constraint_lazy(self, cid: str) -> bool:
return False return False
def find_violated_lazy_constraints(self, model: Any) -> List[Hashable]: def find_violated_lazy_constraints(
self,
solver: "InternalSolver",
model: Any,
) -> List[Hashable]:
""" """
Returns lazy constraint violations found for the current solution. Returns lazy constraint violations found for the current solution.
After solving a model, LearningSolver will ask the instance to identify which After solving a model, LearningSolver will ask the instance to identify which
lazy constraints are violated by the current solution. For each identified lazy constraints are violated by the current solution. For each identified
violation, LearningSolver will then call the build_lazy_constraint, add the violation, LearningSolver will then call the enforce_lazy_constraint and
generated Pyomo constraint to the model, then resolve the problem. The resolve the problem. The process repeats until no further lazy constraint
process repeats until no further lazy constraint violations are found. violations are found.
Each "violation" is simply a string, a tuple or any other hashable type which Each "violation" is simply a string, a tuple or any other hashable type which
allows the instance to identify unambiguously which lazy constraint should be allows the instance to identify unambiguously which lazy constraint should be
generated. In the Traveling Salesman Problem, for example, a subtour generated. In the Traveling Salesman Problem, for example, a subtour
violation could be a frozen set containing the cities in the subtour. violation could be a frozen set containing the cities in the subtour.
The current solution can be queried with `solver.get_solution()`. If the solver
is configured to use lazy callbacks, this solution may be non-integer.
For a concrete example, see TravelingSalesmanInstance. For a concrete example, see TravelingSalesmanInstance.
""" """
return [] return []
def build_lazy_constraint(self, model: Any, violation: Hashable) -> Any: def enforce_lazy_constraint(
self,
solver: "InternalSolver",
model: Any,
violation: Hashable,
) -> None:
""" """
Returns a Pyomo constraint which fixes a given violation. Adds constraints to the model to ensure that the given violation is fixed.
This method is typically called immediately after This method is typically called immediately after
find_violated_lazy_constraints. The violation object provided to this method find_violated_lazy_constraints. The violation object provided to this method
@ -138,11 +152,13 @@ class Instance(ABC):
find_violated_lazy_constraints. After some training, LearningSolver may find_violated_lazy_constraints. After some training, LearningSolver may
decide to proactively build some lazy constraints at the beginning of the decide to proactively build some lazy constraints at the beginning of the
optimization process, before a solution is even available. In this case, optimization process, before a solution is even available. In this case,
build_lazy_constraints will be called without a corresponding call to enforce_lazy_constraints will be called without a corresponding call to
find_violated_lazy_constraints. find_violated_lazy_constraints.
The implementation should not directly add the constraint to the model. The Note that this method can be called either before the optimization starts or
constraint will be added by LearningSolver after the method returns. from within a callback. To ensure that constraints are added correctly in
either case, it is recommended to use `solver.add_constraint`, instead of
modifying the `model` object directly.
For a concrete example, see TravelingSalesmanInstance. For a concrete example, see TravelingSalesmanInstance.
""" """

@ -6,13 +6,16 @@ import gc
import gzip import gzip
import os import os
import pickle import pickle
from typing import Optional, Any, List, Hashable, cast, IO from typing import Optional, Any, List, Hashable, cast, IO, TYPE_CHECKING
from overrides import overrides from overrides import overrides
from miplearn.instance.base import logger, Instance from miplearn.instance.base import logger, Instance
from miplearn.types import VariableName, Category from miplearn.types import VariableName, Category
if TYPE_CHECKING:
from miplearn.solvers.learning import InternalSolver
class PickleGzInstance(Instance): class PickleGzInstance(Instance):
""" """
@ -79,14 +82,23 @@ class PickleGzInstance(Instance):
return self.instance.is_constraint_lazy(cid) return self.instance.is_constraint_lazy(cid)
@overrides @overrides
def find_violated_lazy_constraints(self, model: Any) -> List[Hashable]: def find_violated_lazy_constraints(
self,
solver: "InternalSolver",
model: Any,
) -> List[Hashable]:
assert self.instance is not None assert self.instance is not None
return self.instance.find_violated_lazy_constraints(model) return self.instance.find_violated_lazy_constraints(solver, model)
@overrides @overrides
def build_lazy_constraint(self, model: Any, violation: Hashable) -> Any: def enforce_lazy_constraint(
self,
solver: "InternalSolver",
model: Any,
violation: Hashable,
) -> None:
assert self.instance is not None assert self.instance is not None
return self.instance.build_lazy_constraint(model, violation) self.instance.enforce_lazy_constraint(solver, model, violation)
@overrides @overrides
def find_violated_user_cuts(self, model: Any) -> List[Hashable]: def find_violated_user_cuts(self, model: Any) -> List[Hashable]:
@ -94,9 +106,9 @@ class PickleGzInstance(Instance):
return self.instance.find_violated_user_cuts(model) return self.instance.find_violated_user_cuts(model)
@overrides @overrides
def build_user_cut(self, model: Any, violation: Hashable) -> Any: def build_user_cut(self, model: Any, violation: Hashable) -> None:
assert self.instance is not None assert self.instance is not None
return self.instance.build_user_cut(model, violation) self.instance.build_user_cut(model, violation)
@overrides @overrides
def load(self) -> None: def load(self) -> None:

@ -1,7 +1,7 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020-2021, 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, Tuple, FrozenSet, Any, Optional from typing import List, Tuple, FrozenSet, Any, Optional, Hashable
import networkx as nx import networkx as nx
import numpy as np import numpy as np
@ -11,6 +11,7 @@ from scipy.spatial.distance import pdist, squareform
from scipy.stats import uniform, randint from scipy.stats import uniform, randint
from scipy.stats.distributions import rv_frozen from scipy.stats.distributions import rv_frozen
from miplearn import InternalSolver
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
from miplearn.types import VariableName, Category from miplearn.types import VariableName, Category
@ -85,7 +86,11 @@ class TravelingSalesmanInstance(Instance):
return self.varname_to_index[var_name] return self.varname_to_index[var_name]
@overrides @overrides
def find_violated_lazy_constraints(self, model: Any) -> List[FrozenSet]: def find_violated_lazy_constraints(
self,
solver: InternalSolver,
model: Any,
) -> List[FrozenSet]:
selected_edges = [e for e in self.edges if model.x[e].value > 0.5] selected_edges = [e for e in self.edges if model.x[e].value > 0.5]
graph = nx.Graph() graph = nx.Graph()
graph.add_edges_from(selected_edges) graph.add_edges_from(selected_edges)
@ -97,14 +102,20 @@ class TravelingSalesmanInstance(Instance):
return violations return violations
@overrides @overrides
def build_lazy_constraint(self, model: Any, component: FrozenSet) -> Any: def enforce_lazy_constraint(
self,
solver: InternalSolver,
model: Any,
component: FrozenSet,
) -> None:
cut_edges = [ cut_edges = [
e e
for e in self.edges for e in self.edges
if (e[0] in component and e[1] not in component) if (e[0] in component and e[1] not in component)
or (e[0] not in component and e[1] in component) or (e[0] not in component and e[1] in component)
] ]
return model.eq_subtour.add(sum(model.x[e] for e in cut_edges) >= 2) constr = model.eq_subtour.add(sum(model.x[e] for e in cut_edges) >= 2)
solver.add_constraint(constr)
class TravelingSalesmanGenerator: class TravelingSalesmanGenerator:

@ -232,8 +232,21 @@ class GurobiSolver(InternalSolver):
@overrides @overrides
def get_solution(self) -> Optional[Solution]: def get_solution(self) -> Optional[Solution]:
self._raise_if_callback()
assert self.model is not None assert self.model is not None
if self.cb_where is not None:
if self.cb_where == self.gp.GRB.Callback.MIPNODE:
return {
v.varName: self.model.cbGetNodeRel(v) for v in self.model.getVars()
}
elif self.cb_where == self.gp.GRB.Callback.MIPSOL:
return {
v.varName: self.model.cbGetSolution(v) for v in self.model.getVars()
}
else:
raise Exception(
f"get_solution can only be called from a callback "
f"when cb_where is either MIPNODE or MIPSOL"
)
if self.model.solCount == 0: if self.model.solCount == 0:
return None return None
return {v.varName: v.x for v in self.model.getVars()} return {v.varName: v.x for v in self.model.getVars()}
@ -481,6 +494,7 @@ class GurobiTestInstanceInfeasible(Instance):
class GurobiTestInstanceRedundancy(Instance): class GurobiTestInstanceRedundancy(Instance):
@overrides
def to_model(self) -> Any: def to_model(self) -> Any:
import gurobipy as gp import gurobipy as gp
from gurobipy import GRB from gurobipy import GRB
@ -525,11 +539,10 @@ class GurobiTestInstanceKnapsack(PyomoTestInstanceKnapsack):
return model return model
@overrides @overrides
def build_lazy_constraint(self, model: Any, violation: Hashable) -> Any: def enforce_lazy_constraint(
# TODO: Replace by plain constraint self,
return ExtractedGurobiConstraint( solver: InternalSolver,
lhs=1.0 * model.getVarByName("x[0]"), model: Any,
sense="<", violation: Hashable,
rhs=0.0, ) -> None:
name="cut", solver.add_constraint(model.getVarByName("x[0]") <= 0, name="cut")
)

@ -426,6 +426,7 @@ class PyomoTestInstanceInfeasible(Instance):
class PyomoTestInstanceRedundancy(Instance): class PyomoTestInstanceRedundancy(Instance):
@overrides
def to_model(self) -> pe.ConcreteModel: def to_model(self) -> pe.ConcreteModel:
model = pe.ConcreteModel() model = pe.ConcreteModel()
model.x = pe.Var([0, 1], domain=pe.Binary) model.x = pe.Var([0, 1], domain=pe.Binary)
@ -484,6 +485,11 @@ class PyomoTestInstanceKnapsack(Instance):
] ]
@overrides @overrides
def build_lazy_constraint(self, model: Any, violation: Hashable) -> Any: def enforce_lazy_constraint(
self,
solver: InternalSolver,
model: Any,
violation: Hashable,
) -> None:
model.cut = pe.Constraint(expr=model.x[0] <= 0.0, name="cut") model.cut = pe.Constraint(expr=model.x[0] <= 0.0, name="cut")
return model.cut solver.add_constraint(model.cut)

@ -88,9 +88,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
) )
# Add a brand new constraint # Add a brand new constraint
cut = instance.build_lazy_constraint(model, "cut") instance.enforce_lazy_constraint(solver, model, "cut")
assert cut is not None
solver.add_constraint(cut, name="cut")
# New constraint should be listed # New constraint should be listed
assert_equals( assert_equals(
@ -199,18 +197,16 @@ def run_iteration_cb_tests(solver: InternalSolver) -> None:
def run_lazy_cb_tests(solver: InternalSolver) -> None: def run_lazy_cb_tests(solver: InternalSolver) -> None:
instance = solver.build_test_instance_knapsack() instance = solver.build_test_instance_knapsack()
model = instance.to_model() model = instance.to_model()
lazy_cb_count = 0
def lazy_cb(cb_solver: InternalSolver, cb_model: Any) -> None: def lazy_cb(cb_solver: InternalSolver, cb_model: Any) -> None:
nonlocal lazy_cb_count relsol = cb_solver.get_solution()
lazy_cb_count += 1 assert relsol is not None
cobj = instance.build_lazy_constraint(model, "cut") assert relsol["x[0]"] is not None
if not cb_solver.is_constraint_satisfied(cobj): if relsol["x[0]"] > 0:
cb_solver.add_constraint(cobj) instance.enforce_lazy_constraint(cb_solver, cb_model, "cut")
solver.set_instance(instance, model) solver.set_instance(instance, model)
solver.solve(lazy_cb=lazy_cb) solver.solve(lazy_cb=lazy_cb)
assert lazy_cb_count > 0
solution = solver.get_solution() solution = solver.get_solution()
assert solution is not None assert solution is not None
assert_equals(solution["x[0]"], 0.0) assert_equals(solution["x[0]"], 0.0)

Loading…
Cancel
Save