mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 01:18:52 -06:00
Add user cut callbacks; begin rewrite of UserCutsComponent
This commit is contained in:
@@ -198,6 +198,14 @@ class Component:
|
|||||||
) -> None:
|
) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def user_cut_cb(
|
||||||
|
self,
|
||||||
|
solver: "LearningSolver",
|
||||||
|
instance: Instance,
|
||||||
|
model: Any,
|
||||||
|
) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
def evaluate(self, instances: List[Instance]) -> List:
|
def evaluate(self, instances: List[Instance]) -> List:
|
||||||
ev = []
|
ev = []
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
|
|||||||
64
miplearn/components/user_cuts.py
Normal file
64
miplearn/components/user_cuts.py
Normal file
@@ -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
|
solution: Optional[Solution] = None
|
||||||
upper_bound: Optional[float] = None
|
upper_bound: Optional[float] = None
|
||||||
slacks: Optional[Dict[str, float]] = None
|
slacks: Optional[Dict[str, float]] = None
|
||||||
|
user_cuts_enforced: Optional[Set[Hashable]] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -170,11 +170,14 @@ class Instance(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
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 []
|
return []
|
||||||
|
|
||||||
def build_user_cut(self, model, violation):
|
def build_user_cut(self, model: Any, violation: Hashable) -> Any:
|
||||||
pass
|
return None
|
||||||
|
|
||||||
def flush(self) -> None:
|
def flush(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -185,9 +185,3 @@ class TravelingSalesmanInstance(Instance):
|
|||||||
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)
|
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,
|
LazyCallback,
|
||||||
MIPSolveStats,
|
MIPSolveStats,
|
||||||
)
|
)
|
||||||
from miplearn.types import VarIndex, SolverParams, Solution
|
from miplearn.types import VarIndex, SolverParams, Solution, UserCutCallback
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -153,41 +153,49 @@ class GurobiSolver(InternalSolver):
|
|||||||
tee: bool = False,
|
tee: bool = False,
|
||||||
iteration_cb: IterationCallback = None,
|
iteration_cb: IterationCallback = None,
|
||||||
lazy_cb: LazyCallback = None,
|
lazy_cb: LazyCallback = None,
|
||||||
|
user_cut_cb: UserCutCallback = None,
|
||||||
) -> MIPSolveStats:
|
) -> MIPSolveStats:
|
||||||
self._raise_if_callback()
|
self._raise_if_callback()
|
||||||
assert self.model is not None
|
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):
|
def cb_wrapper(cb_model, cb_where):
|
||||||
try:
|
try:
|
||||||
self.cb_where = cb_where
|
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)
|
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:
|
except:
|
||||||
logger.exception("callback error")
|
logger.exception("callback error")
|
||||||
finally:
|
finally:
|
||||||
self.cb_where = None
|
self.cb_where = None
|
||||||
|
|
||||||
if lazy_cb:
|
# Configure Gurobi
|
||||||
|
if lazy_cb is not None:
|
||||||
self.params["LazyConstraints"] = 1
|
self.params["LazyConstraints"] = 1
|
||||||
|
if user_cut_cb is not None:
|
||||||
|
self.params["PreCrush"] = 1
|
||||||
|
|
||||||
|
# Solve problem
|
||||||
total_wallclock_time = 0
|
total_wallclock_time = 0
|
||||||
total_nodes = 0
|
total_nodes = 0
|
||||||
streams: List[Any] = [StringIO()]
|
streams: List[Any] = [StringIO()]
|
||||||
if tee:
|
if tee:
|
||||||
streams += [sys.stdout]
|
streams += [sys.stdout]
|
||||||
self._apply_params(streams)
|
self._apply_params(streams)
|
||||||
if iteration_cb is None:
|
|
||||||
iteration_cb = lambda: False
|
|
||||||
while True:
|
while True:
|
||||||
with _RedirectOutput(streams):
|
with _RedirectOutput(streams):
|
||||||
if lazy_cb is None:
|
self.model.optimize(cb_wrapper)
|
||||||
self.model.optimize()
|
|
||||||
else:
|
|
||||||
self.model.optimize(cb_wrapper)
|
|
||||||
total_wallclock_time += self.model.runtime
|
total_wallclock_time += self.model.runtime
|
||||||
total_nodes += int(self.model.nodeCount)
|
total_nodes += int(self.model.nodeCount)
|
||||||
should_repeat = iteration_cb()
|
should_repeat = iteration_cb()
|
||||||
if not should_repeat:
|
if not should_repeat:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Fetch results and stats
|
||||||
log = streams[0].getvalue()
|
log = streams[0].getvalue()
|
||||||
ub, lb = None, None
|
ub, lb = None, None
|
||||||
sense = "min" if self.model.modelSense == 1 else "max"
|
sense = "min" if self.model.modelSense == 1 else "max"
|
||||||
@@ -313,6 +321,11 @@ class GurobiSolver(InternalSolver):
|
|||||||
else:
|
else:
|
||||||
self.model.addConstr(constraint, name=name)
|
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:
|
def _clear_warm_start(self) -> None:
|
||||||
for (varname, vardict) in self._all_vars.items():
|
for (varname, vardict) in self._all_vars.items():
|
||||||
for (idx, var) in vardict.items():
|
for (idx, var) in vardict.items():
|
||||||
@@ -421,7 +434,6 @@ class GurobiSolver(InternalSolver):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __setstate__(self, state):
|
def __setstate__(self, state):
|
||||||
|
|
||||||
self.params = state["params"]
|
self.params = state["params"]
|
||||||
self.lazy_cb_where = state["lazy_cb_where"]
|
self.lazy_cb_where = state["lazy_cb_where"]
|
||||||
self.instance = None
|
self.instance = None
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from miplearn.types import (
|
|||||||
Solution,
|
Solution,
|
||||||
BranchPriorities,
|
BranchPriorities,
|
||||||
Constraint,
|
Constraint,
|
||||||
|
UserCutCallback,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -51,6 +52,7 @@ class InternalSolver(ABC):
|
|||||||
tee: bool = False,
|
tee: bool = False,
|
||||||
iteration_cb: IterationCallback = None,
|
iteration_cb: IterationCallback = None,
|
||||||
lazy_cb: LazyCallback = None,
|
lazy_cb: LazyCallback = None,
|
||||||
|
user_cut_cb: UserCutCallback = None,
|
||||||
) -> MIPSolveStats:
|
) -> MIPSolveStats:
|
||||||
"""
|
"""
|
||||||
Solves the currently loaded instance. After this method finishes,
|
Solves the currently loaded instance. After this method finishes,
|
||||||
@@ -72,6 +74,9 @@ class InternalSolver(ABC):
|
|||||||
- Querying if a constraint is satisfied
|
- Querying if a constraint is satisfied
|
||||||
- Adding a new constraint to the problem
|
- Adding a new constraint to the problem
|
||||||
Additional operations may be allowed by specific subclasses.
|
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
|
tee: bool
|
||||||
If true, prints the solver log to the screen.
|
If true, prints the solver log to the screen.
|
||||||
"""
|
"""
|
||||||
@@ -146,7 +151,7 @@ class InternalSolver(ABC):
|
|||||||
`get_solution`. Missing values indicate variables whose priorities
|
`get_solution`. Missing values indicate variables whose priorities
|
||||||
should not be modified.
|
should not be modified.
|
||||||
"""
|
"""
|
||||||
raise Exception("Not implemented")
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_constraint_ids(self) -> List[str]:
|
def get_constraint_ids(self) -> List[str]:
|
||||||
@@ -180,6 +185,13 @@ class InternalSolver(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
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
|
@abstractmethod
|
||||||
def extract_constraint(self, cid: str) -> Constraint:
|
def extract_constraint(self, cid: str) -> Constraint:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -125,18 +125,22 @@ class LearningSolver:
|
|||||||
) -> LearningSolveStats:
|
) -> LearningSolveStats:
|
||||||
|
|
||||||
# Generate model
|
# Generate model
|
||||||
|
# -------------------------------------------------------
|
||||||
if model is None:
|
if model is None:
|
||||||
with _RedirectOutput([]):
|
with _RedirectOutput([]):
|
||||||
model = instance.to_model()
|
model = instance.to_model()
|
||||||
|
|
||||||
# Initialize training sample
|
# Initialize training sample
|
||||||
|
# -------------------------------------------------------
|
||||||
training_sample = TrainingSample()
|
training_sample = TrainingSample()
|
||||||
instance.training_data += [training_sample]
|
instance.training_data += [training_sample]
|
||||||
|
|
||||||
# Initialize stats
|
# Initialize stats
|
||||||
|
# -------------------------------------------------------
|
||||||
stats: LearningSolveStats = {}
|
stats: LearningSolveStats = {}
|
||||||
|
|
||||||
# Initialize internal solver
|
# Initialize internal solver
|
||||||
|
# -------------------------------------------------------
|
||||||
self.tee = tee
|
self.tee = tee
|
||||||
self.internal_solver = self.solver_factory()
|
self.internal_solver = self.solver_factory()
|
||||||
assert self.internal_solver is not None
|
assert self.internal_solver is not None
|
||||||
@@ -144,6 +148,7 @@ class LearningSolver:
|
|||||||
self.internal_solver.set_instance(instance, model)
|
self.internal_solver.set_instance(instance, model)
|
||||||
|
|
||||||
# Extract features
|
# Extract features
|
||||||
|
# -------------------------------------------------------
|
||||||
FeaturesExtractor(self.internal_solver).extract(instance)
|
FeaturesExtractor(self.internal_solver).extract(instance)
|
||||||
|
|
||||||
callback_args = (
|
callback_args = (
|
||||||
@@ -156,6 +161,7 @@ class LearningSolver:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Solve root LP relaxation
|
# Solve root LP relaxation
|
||||||
|
# -------------------------------------------------------
|
||||||
if self.solve_lp:
|
if self.solve_lp:
|
||||||
logger.debug("Running before_solve_lp callbacks...")
|
logger.debug("Running before_solve_lp callbacks...")
|
||||||
for component in self.components.values():
|
for component in self.components.values():
|
||||||
@@ -172,37 +178,50 @@ class LearningSolver:
|
|||||||
for component in self.components.values():
|
for component in self.components.values():
|
||||||
component.after_solve_lp(*callback_args)
|
component.after_solve_lp(*callback_args)
|
||||||
|
|
||||||
# Define wrappers
|
# Callback wrappers
|
||||||
|
# -------------------------------------------------------
|
||||||
def iteration_cb_wrapper() -> bool:
|
def iteration_cb_wrapper() -> bool:
|
||||||
should_repeat = False
|
should_repeat = False
|
||||||
assert isinstance(instance, Instance)
|
|
||||||
for comp in self.components.values():
|
for comp in self.components.values():
|
||||||
if comp.iteration_cb(self, instance, model):
|
if comp.iteration_cb(self, instance, model):
|
||||||
should_repeat = True
|
should_repeat = True
|
||||||
return should_repeat
|
return should_repeat
|
||||||
|
|
||||||
def lazy_cb_wrapper(
|
def lazy_cb_wrapper(
|
||||||
cb_solver: LearningSolver,
|
cb_solver: InternalSolver,
|
||||||
cb_model: Any,
|
cb_model: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert isinstance(instance, Instance)
|
|
||||||
for comp in self.components.values():
|
for comp in self.components.values():
|
||||||
comp.lazy_cb(self, instance, model)
|
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
|
lazy_cb = None
|
||||||
if self.use_lazy_cb:
|
if self.use_lazy_cb:
|
||||||
lazy_cb = lazy_cb_wrapper
|
lazy_cb = lazy_cb_wrapper
|
||||||
|
|
||||||
|
user_cut_cb = None
|
||||||
|
if instance.has_user_cuts():
|
||||||
|
user_cut_cb = user_cut_cb_wrapper
|
||||||
|
|
||||||
# Before-solve callbacks
|
# Before-solve callbacks
|
||||||
|
# -------------------------------------------------------
|
||||||
logger.debug("Running before_solve_mip callbacks...")
|
logger.debug("Running before_solve_mip callbacks...")
|
||||||
for component in self.components.values():
|
for component in self.components.values():
|
||||||
component.before_solve_mip(*callback_args)
|
component.before_solve_mip(*callback_args)
|
||||||
|
|
||||||
# Solve MIP
|
# Solve MIP
|
||||||
|
# -------------------------------------------------------
|
||||||
logger.info("Solving MIP...")
|
logger.info("Solving MIP...")
|
||||||
mip_stats = self.internal_solver.solve(
|
mip_stats = self.internal_solver.solve(
|
||||||
tee=tee,
|
tee=tee,
|
||||||
iteration_cb=iteration_cb_wrapper,
|
iteration_cb=iteration_cb_wrapper,
|
||||||
|
user_cut_cb=user_cut_cb,
|
||||||
lazy_cb=lazy_cb,
|
lazy_cb=lazy_cb,
|
||||||
)
|
)
|
||||||
stats.update(cast(LearningSolveStats, mip_stats))
|
stats.update(cast(LearningSolveStats, mip_stats))
|
||||||
@@ -216,17 +235,20 @@ class LearningSolver:
|
|||||||
stats["Mode"] = self.mode
|
stats["Mode"] = self.mode
|
||||||
|
|
||||||
# Add some information to training_sample
|
# Add some information to training_sample
|
||||||
|
# -------------------------------------------------------
|
||||||
training_sample.lower_bound = stats["Lower bound"]
|
training_sample.lower_bound = stats["Lower bound"]
|
||||||
training_sample.upper_bound = stats["Upper bound"]
|
training_sample.upper_bound = stats["Upper bound"]
|
||||||
training_sample.mip_log = stats["MIP log"]
|
training_sample.mip_log = stats["MIP log"]
|
||||||
training_sample.solution = self.internal_solver.get_solution()
|
training_sample.solution = self.internal_solver.get_solution()
|
||||||
|
|
||||||
# After-solve callbacks
|
# After-solve callbacks
|
||||||
|
# -------------------------------------------------------
|
||||||
logger.debug("Calling after_solve_mip callbacks...")
|
logger.debug("Calling after_solve_mip callbacks...")
|
||||||
for component in self.components.values():
|
for component in self.components.values():
|
||||||
component.after_solve_mip(*callback_args)
|
component.after_solve_mip(*callback_args)
|
||||||
|
|
||||||
# Write to file, if necessary
|
# Flush
|
||||||
|
# -------------------------------------------------------
|
||||||
if not discard_output:
|
if not discard_output:
|
||||||
instance.flush()
|
instance.flush()
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from miplearn.solvers.internal import (
|
|||||||
LazyCallback,
|
LazyCallback,
|
||||||
MIPSolveStats,
|
MIPSolveStats,
|
||||||
)
|
)
|
||||||
from miplearn.types import VarIndex, SolverParams, Solution
|
from miplearn.types import VarIndex, SolverParams, Solution, UserCutCallback
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -81,9 +81,12 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
tee: bool = False,
|
tee: bool = False,
|
||||||
iteration_cb: IterationCallback = None,
|
iteration_cb: IterationCallback = None,
|
||||||
lazy_cb: LazyCallback = None,
|
lazy_cb: LazyCallback = None,
|
||||||
|
user_cut_cb: UserCutCallback = None,
|
||||||
) -> MIPSolveStats:
|
) -> MIPSolveStats:
|
||||||
if lazy_cb is not None:
|
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
|
total_wallclock_time = 0
|
||||||
streams: List[Any] = [StringIO()]
|
streams: List[Any] = [StringIO()]
|
||||||
if tee:
|
if tee:
|
||||||
@@ -318,19 +321,19 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
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 NotImplementedError()
|
||||||
|
|
||||||
def extract_constraint(self, cid: str) -> Constraint:
|
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:
|
def is_constraint_satisfied(self, cobj: Constraint, tol: float = 1e-6) -> bool:
|
||||||
raise Exception("Not implemented")
|
raise NotImplementedError()
|
||||||
|
|
||||||
def is_infeasible(self) -> bool:
|
def is_infeasible(self) -> bool:
|
||||||
return self._termination_condition == TerminationCondition.infeasible
|
return self._termination_condition == TerminationCondition.infeasible
|
||||||
|
|
||||||
def get_dual(self, cid):
|
def get_dual(self, cid):
|
||||||
raise Exception("Not implemented")
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_sense(self) -> str:
|
def get_sense(self) -> str:
|
||||||
return self._obj_sense
|
return self._obj_sense
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
# 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 Optional, Dict, Callable, Any, Union, Tuple
|
from typing import Optional, Dict, Callable, Any, Union, Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
from mypy_extensions import TypedDict
|
from mypy_extensions import TypedDict
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from miplearn.solvers.learning import InternalSolver
|
||||||
|
|
||||||
VarIndex = Union[str, int, Tuple[Union[str, int]]]
|
VarIndex = Union[str, int, Tuple[Union[str, int]]]
|
||||||
|
|
||||||
Solution = Dict[str, Dict[VarIndex, Optional[float]]]
|
Solution = Dict[str, Dict[VarIndex, Optional[float]]]
|
||||||
@@ -64,6 +67,8 @@ IterationCallback = Callable[[], bool]
|
|||||||
|
|
||||||
LazyCallback = Callable[[Any, Any], None]
|
LazyCallback = Callable[[Any, Any], None]
|
||||||
|
|
||||||
|
UserCutCallback = Callable[["InternalSolver", Any], None]
|
||||||
|
|
||||||
SolverParams = Dict[str, Any]
|
SolverParams = Dict[str, Any]
|
||||||
|
|
||||||
BranchPriorities = Solution
|
BranchPriorities = Solution
|
||||||
|
|||||||
@@ -1,31 +1,77 @@
|
|||||||
# 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 Any, List
|
|
||||||
|
|
||||||
import pytest
|
import logging
|
||||||
from networkx import Graph
|
from typing import Any, FrozenSet, Hashable
|
||||||
|
|
||||||
|
import gurobipy as gp
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
from scipy.stats import randint
|
import pytest
|
||||||
|
from gurobipy import GRB
|
||||||
|
from networkx import Graph
|
||||||
|
|
||||||
from miplearn import Instance
|
from miplearn import Instance, LearningSolver, GurobiSolver
|
||||||
from miplearn.problems.stab import MaxWeightStableSetGenerator
|
from miplearn.components.user_cuts import UserCutsComponentNG
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GurobiStableSetProblem(Instance):
|
class GurobiStableSetProblem(Instance):
|
||||||
def __init__(self, graph: Graph) -> None:
|
def __init__(self, graph: Graph) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.graph = graph
|
self.graph = graph
|
||||||
|
self.nodes = list(self.graph.nodes)
|
||||||
|
|
||||||
def to_model(self) -> Any:
|
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
|
@pytest.fixture
|
||||||
def instance() -> Instance:
|
def stab_instance() -> Instance:
|
||||||
graph = nx.generators.random_graphs.binomial_graph(50, 0.5)
|
graph = nx.generators.random_graphs.binomial_graph(50, 0.50, seed=42)
|
||||||
return GurobiStableSetProblem(graph)
|
return GurobiStableSetProblem(graph)
|
||||||
|
|
||||||
|
|
||||||
def test_usage(instance: Instance) -> None:
|
@pytest.fixture
|
||||||
pass
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user