Add user cut callbacks; begin rewrite of UserCutsComponent

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

@ -198,6 +198,14 @@ class Component:
) -> None:
return
def user_cut_cb(
self,
solver: "LearningSolver",
instance: Instance,
model: Any,
) -> None:
return
def evaluate(self, instances: List[Instance]) -> List:
ev = []
for instance in instances:

@ -0,0 +1,64 @@
# 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 Any, TYPE_CHECKING, Hashable, Set
from miplearn import Component, Instance
import logging
from miplearn.features import Features, TrainingSample
from miplearn.types import LearningSolveStats
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from miplearn.solvers.learning import LearningSolver
class UserCutsComponentNG(Component):
def __init__(self) -> None:
self.enforced: Set[Hashable] = set()
def before_solve_mip(
self,
solver: "LearningSolver",
instance: Instance,
model: Any,
stats: LearningSolveStats,
features: Features,
training_data: TrainingSample,
) -> None:
self.enforced.clear()
def after_solve_mip(
self,
solver: "LearningSolver",
instance: Instance,
model: Any,
stats: LearningSolveStats,
features: Features,
training_data: TrainingSample,
) -> None:
training_data.user_cuts_enforced = set(self.enforced)
def user_cut_cb(
self,
solver: "LearningSolver",
instance: Instance,
model: Any,
) -> None:
assert solver.internal_solver is not None
logger.debug("Finding violated user cuts...")
cids = instance.find_violated_user_cuts(model)
logger.debug(f"Found {len(cids)} violated user cuts")
logger.debug("Building violated user cuts...")
for cid in cids:
assert isinstance(cid, Hashable)
cobj = instance.build_user_cut(model, cid)
assert cobj is not None
solver.internal_solver.add_cut(cobj)
self.enforced.add(cid)
if len(cids) > 0:
logger.info(f"Added {len(cids)} violated user cuts")

@ -24,6 +24,7 @@ class TrainingSample:
solution: Optional[Solution] = None
upper_bound: Optional[float] = None
slacks: Optional[Dict[str, float]] = None
user_cuts_enforced: Optional[Set[Hashable]] = None
@dataclass

@ -170,11 +170,14 @@ class Instance(ABC):
"""
pass
def find_violated_user_cuts(self, model):
def has_user_cuts(self) -> bool:
return False
def find_violated_user_cuts(self, model: Any) -> List[Hashable]:
return []
def build_user_cut(self, model, violation):
pass
def build_user_cut(self, model: Any, violation: Hashable) -> Any:
return None
def flush(self) -> None:
"""

@ -185,9 +185,3 @@ class TravelingSalesmanInstance(Instance):
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)
def find_violated_user_cuts(self, model):
return self.find_violated_lazy_constraints(model)
def build_user_cut(self, model, violation):
return self.build_lazy_constraint(model, violation)

@ -17,7 +17,7 @@ from miplearn.solvers.internal import (
LazyCallback,
MIPSolveStats,
)
from miplearn.types import VarIndex, SolverParams, Solution
from miplearn.types import VarIndex, SolverParams, Solution, UserCutCallback
logger = logging.getLogger(__name__)
@ -153,41 +153,49 @@ class GurobiSolver(InternalSolver):
tee: bool = False,
iteration_cb: IterationCallback = None,
lazy_cb: LazyCallback = None,
user_cut_cb: UserCutCallback = None,
) -> MIPSolveStats:
self._raise_if_callback()
assert self.model is not None
if iteration_cb is None:
iteration_cb = lambda: False
# Create callback wrapper
def cb_wrapper(cb_model, cb_where):
try:
self.cb_where = cb_where
if cb_where in self.lazy_cb_where:
if lazy_cb is not None and cb_where in self.lazy_cb_where:
lazy_cb(self, self.model)
if user_cut_cb is not None and cb_where == self.gp.GRB.Callback.MIPNODE:
user_cut_cb(self, self.model)
except:
logger.exception("callback error")
finally:
self.cb_where = None
if lazy_cb:
# Configure Gurobi
if lazy_cb is not None:
self.params["LazyConstraints"] = 1
if user_cut_cb is not None:
self.params["PreCrush"] = 1
# Solve problem
total_wallclock_time = 0
total_nodes = 0
streams: List[Any] = [StringIO()]
if tee:
streams += [sys.stdout]
self._apply_params(streams)
if iteration_cb is None:
iteration_cb = lambda: False
while True:
with _RedirectOutput(streams):
if lazy_cb is None:
self.model.optimize()
else:
self.model.optimize(cb_wrapper)
total_wallclock_time += self.model.runtime
total_nodes += int(self.model.nodeCount)
should_repeat = iteration_cb()
if not should_repeat:
break
# Fetch results and stats
log = streams[0].getvalue()
ub, lb = None, None
sense = "min" if self.model.modelSense == 1 else "max"
@ -313,6 +321,11 @@ class GurobiSolver(InternalSolver):
else:
self.model.addConstr(constraint, name=name)
def add_cut(self, cobj: Any) -> None:
assert self.model is not None
assert self.cb_where == self.gp.GRB.Callback.MIPNODE
self.model.cbCut(cobj)
def _clear_warm_start(self) -> None:
for (varname, vardict) in self._all_vars.items():
for (idx, var) in vardict.items():
@ -421,7 +434,6 @@ class GurobiSolver(InternalSolver):
}
def __setstate__(self, state):
self.params = state["params"]
self.lazy_cb_where = state["lazy_cb_where"]
self.instance = None

@ -16,6 +16,7 @@ from miplearn.types import (
Solution,
BranchPriorities,
Constraint,
UserCutCallback,
)
logger = logging.getLogger(__name__)
@ -51,6 +52,7 @@ class InternalSolver(ABC):
tee: bool = False,
iteration_cb: IterationCallback = None,
lazy_cb: LazyCallback = None,
user_cut_cb: UserCutCallback = None,
) -> MIPSolveStats:
"""
Solves the currently loaded instance. After this method finishes,
@ -72,6 +74,9 @@ class InternalSolver(ABC):
- Querying if a constraint is satisfied
- Adding a new constraint to the problem
Additional operations may be allowed by specific subclasses.
user_cut_cb: UserCutCallback
This function is called whenever the solver found a new integer-infeasible
solution and needs to generate cutting planes to cut it off.
tee: bool
If true, prints the solver log to the screen.
"""
@ -146,7 +151,7 @@ class InternalSolver(ABC):
`get_solution`. Missing values indicate variables whose priorities
should not be modified.
"""
raise Exception("Not implemented")
raise NotImplementedError()
@abstractmethod
def get_constraint_ids(self) -> List[str]:
@ -180,6 +185,13 @@ class InternalSolver(ABC):
"""
pass
def add_cut(self, cobj: Any) -> None:
"""
Adds a cutting plane to the model. This function can only be called from a user
cut callback.
"""
raise NotImplementedError()
@abstractmethod
def extract_constraint(self, cid: str) -> Constraint:
"""

@ -125,18 +125,22 @@ class LearningSolver:
) -> LearningSolveStats:
# Generate model
# -------------------------------------------------------
if model is None:
with _RedirectOutput([]):
model = instance.to_model()
# Initialize training sample
# -------------------------------------------------------
training_sample = TrainingSample()
instance.training_data += [training_sample]
# Initialize stats
# -------------------------------------------------------
stats: LearningSolveStats = {}
# Initialize internal solver
# -------------------------------------------------------
self.tee = tee
self.internal_solver = self.solver_factory()
assert self.internal_solver is not None
@ -144,6 +148,7 @@ class LearningSolver:
self.internal_solver.set_instance(instance, model)
# Extract features
# -------------------------------------------------------
FeaturesExtractor(self.internal_solver).extract(instance)
callback_args = (
@ -156,6 +161,7 @@ class LearningSolver:
)
# Solve root LP relaxation
# -------------------------------------------------------
if self.solve_lp:
logger.debug("Running before_solve_lp callbacks...")
for component in self.components.values():
@ -172,37 +178,50 @@ class LearningSolver:
for component in self.components.values():
component.after_solve_lp(*callback_args)
# Define wrappers
# Callback wrappers
# -------------------------------------------------------
def iteration_cb_wrapper() -> bool:
should_repeat = False
assert isinstance(instance, Instance)
for comp in self.components.values():
if comp.iteration_cb(self, instance, model):
should_repeat = True
return should_repeat
def lazy_cb_wrapper(
cb_solver: LearningSolver,
cb_solver: InternalSolver,
cb_model: Any,
) -> None:
assert isinstance(instance, Instance)
for comp in self.components.values():
comp.lazy_cb(self, instance, model)
def user_cut_cb_wrapper(
cb_solver: InternalSolver,
cb_model: Any,
) -> None:
for comp in self.components.values():
comp.user_cut_cb(self, instance, model)
lazy_cb = None
if self.use_lazy_cb:
lazy_cb = lazy_cb_wrapper
user_cut_cb = None
if instance.has_user_cuts():
user_cut_cb = user_cut_cb_wrapper
# Before-solve callbacks
# -------------------------------------------------------
logger.debug("Running before_solve_mip callbacks...")
for component in self.components.values():
component.before_solve_mip(*callback_args)
# Solve MIP
# -------------------------------------------------------
logger.info("Solving MIP...")
mip_stats = self.internal_solver.solve(
tee=tee,
iteration_cb=iteration_cb_wrapper,
user_cut_cb=user_cut_cb,
lazy_cb=lazy_cb,
)
stats.update(cast(LearningSolveStats, mip_stats))
@ -216,17 +235,20 @@ class LearningSolver:
stats["Mode"] = self.mode
# Add some information to training_sample
# -------------------------------------------------------
training_sample.lower_bound = stats["Lower bound"]
training_sample.upper_bound = stats["Upper bound"]
training_sample.mip_log = stats["MIP log"]
training_sample.solution = self.internal_solver.get_solution()
# After-solve callbacks
# -------------------------------------------------------
logger.debug("Calling after_solve_mip callbacks...")
for component in self.components.values():
component.after_solve_mip(*callback_args)
# Write to file, if necessary
# Flush
# -------------------------------------------------------
if not discard_output:
instance.flush()

@ -23,7 +23,7 @@ from miplearn.solvers.internal import (
LazyCallback,
MIPSolveStats,
)
from miplearn.types import VarIndex, SolverParams, Solution
from miplearn.types import VarIndex, SolverParams, Solution, UserCutCallback
logger = logging.getLogger(__name__)
@ -81,9 +81,12 @@ class BasePyomoSolver(InternalSolver):
tee: bool = False,
iteration_cb: IterationCallback = None,
lazy_cb: LazyCallback = None,
user_cut_cb: UserCutCallback = None,
) -> MIPSolveStats:
if lazy_cb is not None:
raise Exception("lazy callback not supported")
raise Exception("lazy callback not currently supported")
if user_cut_cb is not None:
raise Exception("user cut callback not currently supported")
total_wallclock_time = 0
streams: List[Any] = [StringIO()]
if tee:
@ -318,19 +321,19 @@ class BasePyomoSolver(InternalSolver):
return {}
def set_constraint_sense(self, cid: str, sense: str) -> None:
raise Exception("Not implemented")
raise NotImplementedError()
def extract_constraint(self, cid: str) -> Constraint:
raise Exception("Not implemented")
raise NotImplementedError()
def is_constraint_satisfied(self, cobj: Constraint, tol: float = 1e-6) -> bool:
raise Exception("Not implemented")
raise NotImplementedError()
def is_infeasible(self) -> bool:
return self._termination_condition == TerminationCondition.infeasible
def get_dual(self, cid):
raise Exception("Not implemented")
raise NotImplementedError()
def get_sense(self) -> str:
return self._obj_sense

@ -2,10 +2,13 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
from typing import Optional, Dict, Callable, Any, Union, Tuple
from typing import Optional, Dict, Callable, Any, Union, Tuple, TYPE_CHECKING
from mypy_extensions import TypedDict
if TYPE_CHECKING:
from miplearn.solvers.learning import InternalSolver
VarIndex = Union[str, int, Tuple[Union[str, int]]]
Solution = Dict[str, Dict[VarIndex, Optional[float]]]
@ -64,6 +67,8 @@ IterationCallback = Callable[[], bool]
LazyCallback = Callable[[Any, Any], None]
UserCutCallback = Callable[["InternalSolver", Any], None]
SolverParams = Dict[str, Any]
BranchPriorities = Solution

@ -1,31 +1,77 @@
# 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 Any, List
import logging
from typing import Any, FrozenSet, Hashable
import gurobipy as gp
import networkx as nx
import pytest
from gurobipy import GRB
from networkx import Graph
import networkx as nx
from scipy.stats import randint
from miplearn import Instance
from miplearn.problems.stab import MaxWeightStableSetGenerator
from miplearn import Instance, LearningSolver, GurobiSolver
from miplearn.components.user_cuts import UserCutsComponentNG
logger = logging.getLogger(__name__)
class GurobiStableSetProblem(Instance):
def __init__(self, graph: Graph) -> None:
super().__init__()
self.graph = graph
self.nodes = list(self.graph.nodes)
def to_model(self) -> Any:
pass
model = gp.Model()
x = [model.addVar(vtype=GRB.BINARY) for _ in range(len(self.nodes))]
model.setObjective(gp.quicksum(x), GRB.MAXIMIZE)
for e in list(self.graph.edges):
model.addConstr(x[e[0]] + x[e[1]] <= 1)
return model
def has_user_cuts(self) -> bool:
return True
def find_violated_user_cuts(self, model):
assert isinstance(model, gp.Model)
vals = model.cbGetNodeRel(model.getVars())
violations = []
for clique in nx.find_cliques(self.graph):
lhs = sum(vals[i] for i in clique)
if lhs > 1:
violations += [frozenset(clique)]
return violations
def build_user_cut(self, model: Any, violation: Hashable) -> Any:
assert isinstance(violation, FrozenSet)
x = model.getVars()
cut = gp.quicksum([x[i] for i in violation]) <= 1
return cut
@pytest.fixture
def instance() -> Instance:
graph = nx.generators.random_graphs.binomial_graph(50, 0.5)
def stab_instance() -> Instance:
graph = nx.generators.random_graphs.binomial_graph(50, 0.50, seed=42)
return GurobiStableSetProblem(graph)
def test_usage(instance: Instance) -> None:
pass
@pytest.fixture
def solver() -> LearningSolver:
return LearningSolver(
solver=lambda: GurobiSolver(),
components=[
UserCutsComponentNG(),
],
)
def test_usage(
stab_instance: Instance,
solver: LearningSolver,
) -> None:
solver.solve(stab_instance)
sample = stab_instance.training_data[0]
assert sample.user_cuts_enforced is not None
assert len(sample.user_cuts_enforced) > 0

Loading…
Cancel
Save