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:
assert solver.internal_solver is not None
for cid in cids:
cobj = instance.build_lazy_constraint(model, cid)
solver.internal_solver.add_constraint(cobj)
instance.enforce_lazy_constraint(solver.internal_solver, model, cid)
@overrides
def before_solve_mip(
@ -78,7 +77,7 @@ class DynamicLazyConstraintsComponent(Component):
model: Any,
) -> bool:
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:
logger.debug("No violations found")
return False

@ -4,7 +4,7 @@
import logging
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
@ -13,9 +13,11 @@ from miplearn.types import VariableName, Category
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from miplearn.solvers.learning import InternalSolver
# noinspection PyMethodMayBeStatic
class Instance(ABC):
class Instance(ABC, EnforceOverrides):
"""
Abstract class holding all the data necessary to generate a concrete model of the
proble.
@ -109,28 +111,40 @@ class Instance(ABC):
def is_constraint_lazy(self, cid: str) -> bool:
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.
After solving a model, LearningSolver will ask the instance to identify which
lazy constraints are violated by the current solution. For each identified
violation, LearningSolver will then call the build_lazy_constraint, add the
generated Pyomo constraint to the model, then resolve the problem. The
process repeats until no further lazy constraint violations are found.
violation, LearningSolver will then call the enforce_lazy_constraint and
resolve the problem. The process repeats until no further lazy constraint
violations are found.
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
generated. In the Traveling Salesman Problem, for example, a 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.
"""
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
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
decide to proactively build some lazy constraints at the beginning of the
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.
The implementation should not directly add the constraint to the model. The
constraint will be added by LearningSolver after the method returns.
Note that this method can be called either before the optimization starts or
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.
"""

@ -6,13 +6,16 @@ import gc
import gzip
import os
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 miplearn.instance.base import logger, Instance
from miplearn.types import VariableName, Category
if TYPE_CHECKING:
from miplearn.solvers.learning import InternalSolver
class PickleGzInstance(Instance):
"""
@ -79,14 +82,23 @@ class PickleGzInstance(Instance):
return self.instance.is_constraint_lazy(cid)
@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
return self.instance.find_violated_lazy_constraints(model)
return self.instance.find_violated_lazy_constraints(solver, model)
@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
return self.instance.build_lazy_constraint(model, violation)
self.instance.enforce_lazy_constraint(solver, model, violation)
@overrides
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)
@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
return self.instance.build_user_cut(model, violation)
self.instance.build_user_cut(model, violation)
@overrides
def load(self) -> None:

@ -1,7 +1,7 @@
# 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, Tuple, FrozenSet, Any, Optional
from typing import List, Tuple, FrozenSet, Any, Optional, Hashable
import networkx as nx
import numpy as np
@ -11,6 +11,7 @@ from scipy.spatial.distance import pdist, squareform
from scipy.stats import uniform, randint
from scipy.stats.distributions import rv_frozen
from miplearn import InternalSolver
from miplearn.instance.base import Instance
from miplearn.types import VariableName, Category
@ -85,7 +86,11 @@ class TravelingSalesmanInstance(Instance):
return self.varname_to_index[var_name]
@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]
graph = nx.Graph()
graph.add_edges_from(selected_edges)
@ -97,14 +102,20 @@ class TravelingSalesmanInstance(Instance):
return violations
@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 = [
e
for e in self.edges
if (e[0] in component and e[1] not 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:

@ -232,8 +232,21 @@ class GurobiSolver(InternalSolver):
@overrides
def get_solution(self) -> Optional[Solution]:
self._raise_if_callback()
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:
return None
return {v.varName: v.x for v in self.model.getVars()}
@ -481,6 +494,7 @@ class GurobiTestInstanceInfeasible(Instance):
class GurobiTestInstanceRedundancy(Instance):
@overrides
def to_model(self) -> Any:
import gurobipy as gp
from gurobipy import GRB
@ -525,11 +539,10 @@ class GurobiTestInstanceKnapsack(PyomoTestInstanceKnapsack):
return model
@overrides
def build_lazy_constraint(self, model: Any, violation: Hashable) -> Any:
# TODO: Replace by plain constraint
return ExtractedGurobiConstraint(
lhs=1.0 * model.getVarByName("x[0]"),
sense="<",
rhs=0.0,
name="cut",
)
def enforce_lazy_constraint(
self,
solver: InternalSolver,
model: Any,
violation: Hashable,
) -> None:
solver.add_constraint(model.getVarByName("x[0]") <= 0, name="cut")

@ -426,6 +426,7 @@ class PyomoTestInstanceInfeasible(Instance):
class PyomoTestInstanceRedundancy(Instance):
@overrides
def to_model(self) -> pe.ConcreteModel:
model = pe.ConcreteModel()
model.x = pe.Var([0, 1], domain=pe.Binary)
@ -484,6 +485,11 @@ class PyomoTestInstanceKnapsack(Instance):
]
@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")
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
cut = instance.build_lazy_constraint(model, "cut")
assert cut is not None
solver.add_constraint(cut, name="cut")
instance.enforce_lazy_constraint(solver, model, "cut")
# New constraint should be listed
assert_equals(
@ -199,18 +197,16 @@ def run_iteration_cb_tests(solver: InternalSolver) -> None:
def run_lazy_cb_tests(solver: InternalSolver) -> None:
instance = solver.build_test_instance_knapsack()
model = instance.to_model()
lazy_cb_count = 0
def lazy_cb(cb_solver: InternalSolver, cb_model: Any) -> None:
nonlocal lazy_cb_count
lazy_cb_count += 1
cobj = instance.build_lazy_constraint(model, "cut")
if not cb_solver.is_constraint_satisfied(cobj):
cb_solver.add_constraint(cobj)
relsol = cb_solver.get_solution()
assert relsol is not None
assert relsol["x[0]"] is not None
if relsol["x[0]"] > 0:
instance.enforce_lazy_constraint(cb_solver, cb_model, "cut")
solver.set_instance(instance, model)
solver.solve(lazy_cb=lazy_cb)
assert lazy_cb_count > 0
solution = solver.get_solution()
assert solution is not None
assert_equals(solution["x[0]"], 0.0)

Loading…
Cancel
Save