mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Replace build_lazy_constraint by enforce_lazy_constraint
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user