Allow user to attach arbitrary data to violations

master
Alinson S. Xavier 4 years ago
parent ba8f5bb2f4
commit 2a76dd42ec
No known key found for this signature in database
GPG Key ID: DCA0DAD4D2F58624

@ -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.
import json
import logging import logging
from typing import Dict, List, Tuple, Optional, Any, Set from typing import Dict, List, Tuple, Optional, Any, Set
@ -36,7 +36,7 @@ class DynamicConstraintsComponent(Component):
self.classifier_prototype: Classifier = classifier self.classifier_prototype: Classifier = classifier
self.classifiers: Dict[ConstraintCategory, Classifier] = {} self.classifiers: Dict[ConstraintCategory, Classifier] = {}
self.thresholds: Dict[ConstraintCategory, Threshold] = {} self.thresholds: Dict[ConstraintCategory, Threshold] = {}
self.known_cids: List[ConstraintName] = [] self.known_violations: Dict[ConstraintName, Any] = {}
self.attr = attr self.attr = attr
def sample_xy_with_cids( def sample_xy_with_cids(
@ -48,18 +48,19 @@ class DynamicConstraintsComponent(Component):
Dict[ConstraintCategory, List[List[bool]]], Dict[ConstraintCategory, List[List[bool]]],
Dict[ConstraintCategory, List[ConstraintName]], Dict[ConstraintCategory, List[ConstraintName]],
]: ]:
if len(self.known_cids) == 0: if len(self.known_violations) == 0:
return {}, {}, {} return {}, {}, {}
assert instance is not None assert instance is not None
x: Dict[ConstraintCategory, List[List[float]]] = {} x: Dict[ConstraintCategory, List[List[float]]] = {}
y: Dict[ConstraintCategory, List[List[bool]]] = {} y: Dict[ConstraintCategory, List[List[bool]]] = {}
cids: Dict[ConstraintCategory, List[ConstraintName]] = {} cids: Dict[ConstraintCategory, List[ConstraintName]] = {}
known_cids = np.array(self.known_cids, dtype="S") known_cids = np.array(sorted(list(self.known_violations.keys())), dtype="S")
enforced_cids = None enforced_cids = None
enforced_cids_np = sample.get_array(self.attr) enforced_encoded = sample.get_scalar(self.attr)
if enforced_cids_np is not None: if enforced_encoded is not None:
enforced_cids = list(enforced_cids_np) enforced = self.decode(enforced_encoded)
enforced_cids = list(enforced.keys())
# Get user-provided constraint features # Get user-provided constraint features
( (
@ -100,11 +101,10 @@ class DynamicConstraintsComponent(Component):
@overrides @overrides
def pre_fit(self, pre: List[Any]) -> None: def pre_fit(self, pre: List[Any]) -> None:
assert pre is not None assert pre is not None
known_cids: Set = set() self.known_violations.clear()
for cids in pre: for violations in pre:
known_cids |= set(list(cids)) for (vname, vdata) in violations.items():
self.known_cids.clear() self.known_violations[vname] = vdata
self.known_cids.extend(sorted(known_cids))
def sample_predict( def sample_predict(
self, self,
@ -112,7 +112,7 @@ class DynamicConstraintsComponent(Component):
sample: Sample, sample: Sample,
) -> List[ConstraintName]: ) -> List[ConstraintName]:
pred: List[ConstraintName] = [] pred: List[ConstraintName] = []
if len(self.known_cids) == 0: if len(self.known_violations) == 0:
logger.info("Classifiers not fitted. Skipping.") logger.info("Classifiers not fitted. Skipping.")
return pred return pred
x, _, cids = self.sample_xy_with_cids(instance, sample) x, _, cids = self.sample_xy_with_cids(instance, sample)
@ -131,7 +131,9 @@ class DynamicConstraintsComponent(Component):
@overrides @overrides
def pre_sample_xy(self, instance: Instance, sample: Sample) -> Any: def pre_sample_xy(self, instance: Instance, sample: Sample) -> Any:
return sample.get_array(self.attr) attr_encoded = sample.get_scalar(self.attr)
assert attr_encoded is not None
return self.decode(attr_encoded)
@overrides @overrides
def fit_xy( def fit_xy(
@ -153,11 +155,13 @@ class DynamicConstraintsComponent(Component):
instance: Instance, instance: Instance,
sample: Sample, sample: Sample,
) -> Dict[str, float]: ) -> Dict[str, float]:
actual = sample.get_array(self.attr) attr_encoded = sample.get_scalar(self.attr)
assert actual is not None assert attr_encoded is not None
actual_violations = DynamicConstraintsComponent.decode(attr_encoded)
actual = set(actual_violations.keys())
pred = set(self.sample_predict(instance, sample)) pred = set(self.sample_predict(instance, sample))
tp, tn, fp, fn = 0, 0, 0, 0 tp, tn, fp, fn = 0, 0, 0, 0
for cid in self.known_cids: for cid in self.known_violations.keys():
if cid in pred: if cid in pred:
if cid in actual: if cid in actual:
tp += 1 tp += 1
@ -169,3 +173,12 @@ class DynamicConstraintsComponent(Component):
else: else:
tn += 1 tn += 1
return classifier_evaluation_dict(tp=tp, tn=tn, fp=fp, fn=fn) return classifier_evaluation_dict(tp=tp, tn=tn, fp=fp, fn=fn)
@staticmethod
def encode(violations: Dict[ConstraintName, Any]) -> str:
return json.dumps({k.decode(): v for (k, v) in violations.items()})
@staticmethod
def decode(violations_encoded: str) -> Dict[ConstraintName, Any]:
violations = json.loads(violations_encoded)
return {k.encode(): v for (k, v) in violations.items()}

@ -1,10 +1,8 @@
# 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.
import logging import logging
import pdb from typing import Dict, List, TYPE_CHECKING, Tuple, Any, Optional
from typing import Dict, List, TYPE_CHECKING, Tuple, Any, Optional, Set
import numpy as np import numpy as np
from overrides import overrides from overrides import overrides
@ -37,23 +35,23 @@ class DynamicLazyConstraintsComponent(Component):
self.dynamic: DynamicConstraintsComponent = DynamicConstraintsComponent( self.dynamic: DynamicConstraintsComponent = DynamicConstraintsComponent(
classifier=classifier, classifier=classifier,
threshold=threshold, threshold=threshold,
attr="mip_constr_lazy_enforced", attr="mip_constr_lazy",
) )
self.classifiers = self.dynamic.classifiers self.classifiers = self.dynamic.classifiers
self.thresholds = self.dynamic.thresholds self.thresholds = self.dynamic.thresholds
self.known_cids = self.dynamic.known_cids self.known_violations = self.dynamic.known_violations
self.lazy_enforced: Set[ConstraintName] = set() self.lazy_enforced: Dict[ConstraintName, Any] = {}
@staticmethod @staticmethod
def enforce( def enforce(
cids: List[ConstraintName], violations: Dict[ConstraintName, Any],
instance: Instance, instance: Instance,
model: Any, model: Any,
solver: "LearningSolver", solver: "LearningSolver",
) -> None: ) -> None:
assert solver.internal_solver is not None assert solver.internal_solver is not None
for cid in cids: for (vname, vdata) in violations.items():
instance.enforce_lazy_constraint(solver.internal_solver, model, cid) instance.enforce_lazy_constraint(solver.internal_solver, model, vdata)
@overrides @overrides
def before_solve_mip( def before_solve_mip(
@ -66,9 +64,10 @@ class DynamicLazyConstraintsComponent(Component):
) -> None: ) -> None:
self.lazy_enforced.clear() self.lazy_enforced.clear()
logger.info("Predicting violated (dynamic) lazy constraints...") logger.info("Predicting violated (dynamic) lazy constraints...")
cids = self.dynamic.sample_predict(instance, sample) vnames = self.dynamic.sample_predict(instance, sample)
logger.info("Enforcing %d lazy constraints..." % len(cids)) violations = {c: self.dynamic.known_violations[c] for c in vnames}
self.enforce(cids, instance, model, solver) logger.info("Enforcing %d lazy constraints..." % len(vnames))
self.enforce(violations, instance, model, solver)
@overrides @overrides
def after_solve_mip( def after_solve_mip(
@ -79,10 +78,7 @@ class DynamicLazyConstraintsComponent(Component):
stats: LearningSolveStats, stats: LearningSolveStats,
sample: Sample, sample: Sample,
) -> None: ) -> None:
sample.put_array( sample.put_scalar("mip_constr_lazy", self.dynamic.encode(self.lazy_enforced))
"mip_constr_lazy_enforced",
np.array(list(self.lazy_enforced), dtype="S"),
)
@overrides @overrides
def iteration_cb( def iteration_cb(
@ -93,14 +89,17 @@ class DynamicLazyConstraintsComponent(Component):
) -> bool: ) -> bool:
assert solver.internal_solver is not None assert solver.internal_solver is not None
logger.debug("Finding violated lazy constraints...") logger.debug("Finding violated lazy constraints...")
cids = instance.find_violated_lazy_constraints(solver.internal_solver, model) violations = instance.find_violated_lazy_constraints(
if len(cids) == 0: solver.internal_solver, model
)
if len(violations) == 0:
logger.debug("No violations found") logger.debug("No violations found")
return False return False
else: else:
self.lazy_enforced |= set(cids) for v in violations:
logger.debug(" %d violations found" % len(cids)) self.lazy_enforced[v] = violations[v]
self.enforce(cids, instance, model, solver) logger.debug(" %d violations found" % len(violations))
self.enforce(violations, instance, model, solver)
return True return True
# Delegate ML methods to self.dynamic # Delegate ML methods to self.dynamic

@ -1,9 +1,8 @@
# 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.
import logging import logging
from typing import Any, TYPE_CHECKING, Set, Tuple, Dict, List, Optional from typing import Any, TYPE_CHECKING, Tuple, Dict, List
import numpy as np import numpy as np
from overrides import overrides from overrides import overrides
@ -32,9 +31,9 @@ class UserCutsComponent(Component):
self.dynamic = DynamicConstraintsComponent( self.dynamic = DynamicConstraintsComponent(
classifier=classifier, classifier=classifier,
threshold=threshold, threshold=threshold,
attr="mip_user_cuts_enforced", attr="mip_user_cuts",
) )
self.enforced: Set[ConstraintName] = set() self.enforced: Dict[ConstraintName, Any] = {}
self.n_added_in_callback = 0 self.n_added_in_callback = 0
@overrides @overrides
@ -50,11 +49,12 @@ class UserCutsComponent(Component):
self.enforced.clear() self.enforced.clear()
self.n_added_in_callback = 0 self.n_added_in_callback = 0
logger.info("Predicting violated user cuts...") logger.info("Predicting violated user cuts...")
cids = self.dynamic.sample_predict(instance, sample) vnames = self.dynamic.sample_predict(instance, sample)
logger.info("Enforcing %d user cuts ahead-of-time..." % len(cids)) logger.info("Enforcing %d user cuts ahead-of-time..." % len(vnames))
for cid in cids: for vname in vnames:
instance.enforce_user_cut(solver.internal_solver, model, cid) vdata = self.dynamic.known_violations[vname]
stats["UserCuts: Added ahead-of-time"] = len(cids) instance.enforce_user_cut(solver.internal_solver, model, vdata)
stats["UserCuts: Added ahead-of-time"] = len(vnames)
@overrides @overrides
def user_cut_cb( def user_cut_cb(
@ -65,18 +65,17 @@ class UserCutsComponent(Component):
) -> None: ) -> None:
assert solver.internal_solver is not None assert solver.internal_solver is not None
logger.debug("Finding violated user cuts...") logger.debug("Finding violated user cuts...")
cids = instance.find_violated_user_cuts(model) violations = instance.find_violated_user_cuts(model)
logger.debug(f"Found {len(cids)} violated user cuts") logger.debug(f"Found {len(violations)} violated user cuts")
logger.debug("Building violated user cuts...") logger.debug("Building violated user cuts...")
for cid in cids: for (vname, vdata) in violations.items():
if cid in self.enforced: if vname in self.enforced:
continue continue
assert isinstance(cid, ConstraintName) instance.enforce_user_cut(solver.internal_solver, model, vdata)
instance.enforce_user_cut(solver.internal_solver, model, cid) self.enforced[vname] = vdata
self.enforced.add(cid)
self.n_added_in_callback += 1 self.n_added_in_callback += 1
if len(cids) > 0: if len(violations) > 0:
logger.debug(f"Added {len(cids)} violated user cuts") logger.debug(f"Added {len(violations)} violated user cuts")
@overrides @overrides
def after_solve_mip( def after_solve_mip(
@ -87,10 +86,7 @@ class UserCutsComponent(Component):
stats: LearningSolveStats, stats: LearningSolveStats,
sample: Sample, sample: Sample,
) -> None: ) -> None:
sample.put_array( sample.put_scalar("mip_user_cuts", self.dynamic.encode(self.enforced))
"mip_user_cuts_enforced",
np.array(list(self.enforced), dtype="S"),
)
stats["UserCuts: Added in callback"] = self.n_added_in_callback stats["UserCuts: Added in callback"] = self.n_added_in_callback
if self.n_added_in_callback > 0: if self.n_added_in_callback > 0:
logger.info(f"{self.n_added_in_callback} user cuts added in callback") logger.info(f"{self.n_added_in_callback} user cuts added in callback")
@ -133,5 +129,5 @@ class UserCutsComponent(Component):
self, self,
instance: "Instance", instance: "Instance",
sample: Sample, sample: Sample,
) -> Dict[ConstraintCategory, Dict[str, float]]: ) -> Dict[ConstraintCategory, Dict[ConstraintName, float]]:
return self.dynamic.sample_evaluate(instance, sample) return self.dynamic.sample_evaluate(instance, sample)

@ -9,7 +9,7 @@ from typing import Any, List, TYPE_CHECKING, Dict
import numpy as np import numpy as np
from miplearn.features.sample import Sample, MemorySample from miplearn.features.sample import Sample, MemorySample
from miplearn.types import ConstraintName, ConstraintCategory from miplearn.types import ConstraintName
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -114,7 +114,7 @@ class Instance(ABC):
self, self,
solver: "InternalSolver", solver: "InternalSolver",
model: Any, model: Any,
) -> List[ConstraintName]: ) -> Dict[ConstraintName, Any]:
""" """
Returns lazy constraint violations found for the current solution. Returns lazy constraint violations found for the current solution.
@ -124,40 +124,46 @@ class Instance(ABC):
resolve the problem. The process repeats until no further lazy constraint resolve the problem. The process repeats until no further lazy constraint
violations are found. violations are found.
Each "violation" is simply a string which allows the instance to identify Violations should be returned in a dictionary mapping the name of the violation
unambiguously which lazy constraint should be generated. In the Traveling to some user-specified data that allows the instance to unambiguously generate
Salesman Problem, for example, a subtour violation could be a string the lazy constraints at a later time. In the Traveling Salesman Problem, for
containing the cities in the subtour. example, this function could return a dictionary identifying violated subtour
inequalities. More concretely, it could return:
{
"s1": [1, 2, 3],
"s2": [4, 5, 6, 7],
}
where "s1" and "s2" are the names of the subtours, and [1,2,3] and [4,5,6,7]
are the cities in each subtour. The names of the violations should be kept
stable across instances. In our example, "s1" should always correspond to
[1,2,3] across all instances. The user-provided data should be picklable.
The current solution can be queried with `solver.get_solution()`. If the solver 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. 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 enforce_lazy_constraint( def enforce_lazy_constraint(
self, self,
solver: "InternalSolver", solver: "InternalSolver",
model: Any, model: Any,
violation: ConstraintName, violation_data: Any,
) -> None: ) -> None:
""" """
Adds constraints to the model to ensure that the given violation is fixed. 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 argument `violation_data` is the
is exactly the same object returned earlier by user-provided data, previously returned by `find_violated_lazy_constraints`.
find_violated_lazy_constraints. After some training, LearningSolver may In the Traveling Salesman Problem, for example, it could be a list of cities
decide to proactively build some lazy constraints at the beginning of the in the subtour.
optimization process, before a solution is even available. In this case,
enforce_lazy_constraints will be called without a corresponding call to After some training, LearningSolver may decide to proactively build some lazy
find_violated_lazy_constraints. constraints at the beginning of the optimization process, before a solution
is even available. In this case, `enforce_lazy_constraints` will be called
Note that this method can be called either before the optimization starts or without a corresponding call to `find_violated_lazy_constraints`.
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.
""" """
@ -166,14 +172,14 @@ class Instance(ABC):
def has_user_cuts(self) -> bool: def has_user_cuts(self) -> bool:
return False return False
def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]: def find_violated_user_cuts(self, model: Any) -> Dict[ConstraintName, Any]:
return [] return {}
def enforce_user_cut( def enforce_user_cut(
self, self,
solver: "InternalSolver", solver: "InternalSolver",
model: Any, model: Any,
violation: ConstraintName, violation_data: Any,
) -> Any: ) -> Any:
return None return None

@ -3,15 +3,15 @@
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import gc import gc
import os import os
from typing import Any, Optional, List, Dict, TYPE_CHECKING
import pickle import pickle
from typing import Any, Optional, List, Dict, TYPE_CHECKING
import numpy as np import numpy as np
from overrides import overrides from overrides import overrides
from miplearn.features.sample import Hdf5Sample, Sample from miplearn.features.sample import Hdf5Sample, Sample
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
from miplearn.types import ConstraintName, ConstraintCategory from miplearn.types import ConstraintName
if TYPE_CHECKING: if TYPE_CHECKING:
from miplearn.solvers.learning import InternalSolver from miplearn.solvers.learning import InternalSolver
@ -71,7 +71,7 @@ class FileInstance(Instance):
self, self,
solver: "InternalSolver", solver: "InternalSolver",
model: Any, model: Any,
) -> List[ConstraintName]: ) -> Dict[ConstraintName, Any]:
assert self.instance is not None assert self.instance is not None
return self.instance.find_violated_lazy_constraints(solver, model) return self.instance.find_violated_lazy_constraints(solver, model)
@ -80,13 +80,13 @@ class FileInstance(Instance):
self, self,
solver: "InternalSolver", solver: "InternalSolver",
model: Any, model: Any,
violation: ConstraintName, violation_data: Any,
) -> None: ) -> None:
assert self.instance is not None assert self.instance is not None
self.instance.enforce_lazy_constraint(solver, model, violation) self.instance.enforce_lazy_constraint(solver, model, violation_data)
@overrides @overrides
def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]: def find_violated_user_cuts(self, model: Any) -> Dict[ConstraintName, Any]:
assert self.instance is not None assert self.instance is not None
return self.instance.find_violated_user_cuts(model) return self.instance.find_violated_user_cuts(model)
@ -95,10 +95,10 @@ class FileInstance(Instance):
self, self,
solver: "InternalSolver", solver: "InternalSolver",
model: Any, model: Any,
violation: ConstraintName, violation_data: Any,
) -> None: ) -> None:
assert self.instance is not None assert self.instance is not None
self.instance.enforce_user_cut(solver, model, violation) self.instance.enforce_user_cut(solver, model, violation_data)
# Input & Output # Input & Output
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------

@ -13,7 +13,7 @@ from overrides import overrides
from miplearn.features.sample import Sample from miplearn.features.sample import Sample
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
from miplearn.types import ConstraintName, ConstraintCategory from miplearn.types import ConstraintName
if TYPE_CHECKING: if TYPE_CHECKING:
from miplearn.solvers.learning import InternalSolver from miplearn.solvers.learning import InternalSolver
@ -83,7 +83,7 @@ class PickleGzInstance(Instance):
self, self,
solver: "InternalSolver", solver: "InternalSolver",
model: Any, model: Any,
) -> List[ConstraintName]: ) -> Dict[ConstraintName, Any]:
assert self.instance is not None assert self.instance is not None
return self.instance.find_violated_lazy_constraints(solver, model) return self.instance.find_violated_lazy_constraints(solver, model)
@ -92,13 +92,13 @@ class PickleGzInstance(Instance):
self, self,
solver: "InternalSolver", solver: "InternalSolver",
model: Any, model: Any,
violation: ConstraintName, violation_data: Any,
) -> None: ) -> None:
assert self.instance is not None assert self.instance is not None
self.instance.enforce_lazy_constraint(solver, model, violation) self.instance.enforce_lazy_constraint(solver, model, violation_data)
@overrides @overrides
def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]: def find_violated_user_cuts(self, model: Any) -> Dict[ConstraintName, Any]:
assert self.instance is not None assert self.instance is not None
return self.instance.find_violated_user_cuts(model) return self.instance.find_violated_user_cuts(model)
@ -107,10 +107,10 @@ class PickleGzInstance(Instance):
self, self,
solver: "InternalSolver", solver: "InternalSolver",
model: Any, model: Any,
violation: ConstraintName, violation_name: Any,
) -> None: ) -> None:
assert self.instance is not None assert self.instance is not None
self.instance.enforce_user_cut(solver, model, violation) self.instance.enforce_user_cut(solver, model, violation_name)
@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, Dict from typing import List, Tuple, Any, Optional, Dict
import networkx as nx import networkx as nx
import numpy as np import numpy as np
@ -86,14 +86,15 @@ class TravelingSalesmanInstance(Instance):
self, self,
solver: InternalSolver, solver: InternalSolver,
model: Any, model: Any,
) -> List[ConstraintName]: ) -> Dict[ConstraintName, List]:
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)
violations = [] violations = {}
for c in list(nx.connected_components(graph)): for c in list(nx.connected_components(graph)):
if len(c) < self.n_cities: if len(c) < self.n_cities:
violations.append(",".join(map(str, c)).encode()) cname = ("st[" + ",".join(map(str, c)) + "]").encode()
violations[cname] = list(c)
return violations return violations
@overrides @overrides
@ -101,10 +102,9 @@ class TravelingSalesmanInstance(Instance):
self, self,
solver: InternalSolver, solver: InternalSolver,
model: Any, model: Any,
violation: ConstraintName, component: List,
) -> None: ) -> None:
assert isinstance(solver, BasePyomoSolver) assert isinstance(solver, BasePyomoSolver)
component = [int(v) for v in violation.decode().split(",")]
cut_edges = [ cut_edges = [
e e
for e in self.edges for e in self.edges

@ -710,7 +710,7 @@ class GurobiTestInstanceKnapsack(PyomoTestInstanceKnapsack):
self, self,
solver: InternalSolver, solver: InternalSolver,
model: Any, model: Any,
violation: str, violation_data: Any,
) -> None: ) -> None:
x0 = model.getVarByName("x[0]") x0 = model.getVarByName("x[0]")
model.cbLazy(x0 <= 0) model.cbLazy(x0 <= 0)

@ -247,7 +247,7 @@ def run_lazy_cb_tests(solver: InternalSolver) -> None:
assert relsol is not None assert relsol is not None
assert relsol[b"x[0]"] is not None assert relsol[b"x[0]"] is not None
if relsol[b"x[0]"] > 0: if relsol[b"x[0]"] > 0:
instance.enforce_lazy_constraint(cb_solver, cb_model, b"cut") instance.enforce_lazy_constraint(cb_solver, cb_model, None)
solver.set_instance(instance, model) solver.set_instance(instance, model)
solver.solve(lazy_cb=lazy_cb) solver.solve(lazy_cb=lazy_cb)

@ -10,6 +10,7 @@ import pytest
from miplearn.classifiers import Classifier from miplearn.classifiers import Classifier
from miplearn.classifiers.threshold import MinProbabilityThreshold from miplearn.classifiers.threshold import MinProbabilityThreshold
from miplearn.components import classifier_evaluation_dict from miplearn.components import classifier_evaluation_dict
from miplearn.components.dynamic_common import DynamicConstraintsComponent
from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent
from miplearn.features.sample import MemorySample from miplearn.features.sample import MemorySample
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
@ -24,13 +25,23 @@ def training_instances() -> List[Instance]:
samples_0 = [ samples_0 = [
MemorySample( MemorySample(
{ {
"mip_constr_lazy_enforced": np.array(["c1", "c2"], dtype="S"), "mip_constr_lazy": DynamicConstraintsComponent.encode(
{
b"c1": 0,
b"c2": 0,
}
),
"static_instance_features": np.array([5.0]), "static_instance_features": np.array([5.0]),
}, },
), ),
MemorySample( MemorySample(
{ {
"mip_constr_lazy_enforced": np.array(["c2", "c3"], dtype="S"), "mip_constr_lazy": DynamicConstraintsComponent.encode(
{
b"c2": 0,
b"c3": 0,
}
),
"static_instance_features": np.array([5.0]), "static_instance_features": np.array([5.0]),
}, },
), ),
@ -55,7 +66,12 @@ def training_instances() -> List[Instance]:
samples_1 = [ samples_1 = [
MemorySample( MemorySample(
{ {
"mip_constr_lazy_enforced": np.array(["c3", "c4"], dtype="S"), "mip_constr_lazy": DynamicConstraintsComponent.encode(
{
b"c3": 0,
b"c4": 0,
}
),
"static_instance_features": np.array([8.0]), "static_instance_features": np.array([8.0]),
}, },
) )
@ -83,8 +99,8 @@ def test_sample_xy(training_instances: List[Instance]) -> None:
comp = DynamicLazyConstraintsComponent() comp = DynamicLazyConstraintsComponent()
comp.pre_fit( comp.pre_fit(
[ [
np.array(["c1", "c3", "c4"], dtype="S"), {b"c1": 0, b"c3": 0, b"c4": 0},
np.array(["c1", "c2", "c4"], dtype="S"), {b"c1": 0, b"c2": 0, b"c4": 0},
] ]
) )
x_expected = { x_expected = {
@ -105,7 +121,10 @@ def test_sample_xy(training_instances: List[Instance]) -> None:
def test_sample_predict_evaluate(training_instances: List[Instance]) -> None: def test_sample_predict_evaluate(training_instances: List[Instance]) -> None:
comp = DynamicLazyConstraintsComponent() comp = DynamicLazyConstraintsComponent()
comp.known_cids.extend([b"c1", b"c2", b"c3", b"c4"]) comp.known_violations[b"c1"] = 0
comp.known_violations[b"c2"] = 0
comp.known_violations[b"c3"] = 0
comp.known_violations[b"c4"] = 0
comp.thresholds[b"type-a"] = MinProbabilityThreshold([0.5, 0.5]) comp.thresholds[b"type-a"] = MinProbabilityThreshold([0.5, 0.5])
comp.thresholds[b"type-b"] = MinProbabilityThreshold([0.5, 0.5]) comp.thresholds[b"type-b"] = MinProbabilityThreshold([0.5, 0.5])
comp.classifiers[b"type-a"] = Mock(spec=Classifier) comp.classifiers[b"type-a"] = Mock(spec=Classifier)

@ -1,9 +1,9 @@
# 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.
import json
import logging import logging
from typing import Any, FrozenSet, List from typing import Any, List, Dict
import gurobipy as gp import gurobipy as gp
import networkx as nx import networkx as nx
@ -12,12 +12,11 @@ from gurobipy import GRB
from networkx import Graph from networkx import Graph
from overrides import overrides from overrides import overrides
from miplearn.solvers.learning import InternalSolver
from miplearn.components.dynamic_user_cuts import UserCutsComponent from miplearn.components.dynamic_user_cuts import UserCutsComponent
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.gurobi import GurobiSolver
from miplearn.solvers.learning import LearningSolver from miplearn.solvers.learning import LearningSolver
from miplearn.types import ConstraintName, ConstraintCategory from miplearn.types import ConstraintName
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -41,13 +40,14 @@ class GurobiStableSetProblem(Instance):
return True return True
@overrides @overrides
def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]: def find_violated_user_cuts(self, model: Any) -> Dict[ConstraintName, Any]:
assert isinstance(model, gp.Model) assert isinstance(model, gp.Model)
vals = model.cbGetNodeRel(model.getVars()) vals = model.cbGetNodeRel(model.getVars())
violations = [] violations = {}
for clique in nx.find_cliques(self.graph): for clique in nx.find_cliques(self.graph):
if sum(vals[i] for i in clique) > 1: if sum(vals[i] for i in clique) > 1:
violations.append(",".join([str(i) for i in clique]).encode()) vname = (",".join([str(i) for i in clique])).encode()
violations[vname] = list(clique)
return violations return violations
@overrides @overrides
@ -55,9 +55,8 @@ class GurobiStableSetProblem(Instance):
self, self,
solver: GurobiSolver, solver: GurobiSolver,
model: Any, model: Any,
cid: ConstraintName, clique: List[int],
) -> Any: ) -> Any:
clique = [int(i) for i in cid.decode().split(",")]
x = model.getVars() x = model.getVars()
constr = gp.quicksum([x[i] for i in clique]) <= 1 constr = gp.quicksum([x[i] for i in clique]) <= 1
if solver.cb_where: if solver.cb_where:
@ -86,9 +85,11 @@ def test_usage(
) -> None: ) -> None:
stats_before = solver.solve(stab_instance) stats_before = solver.solve(stab_instance)
sample = stab_instance.get_samples()[0] sample = stab_instance.get_samples()[0]
user_cuts_enforced = sample.get_array("mip_user_cuts_enforced") user_cuts_encoded = sample.get_scalar("mip_user_cuts")
assert user_cuts_enforced is not None assert user_cuts_encoded is not None
assert len(user_cuts_enforced) > 0 user_cuts = json.loads(user_cuts_encoded)
assert user_cuts is not None
assert len(user_cuts) > 0
assert stats_before["UserCuts: Added ahead-of-time"] == 0 assert stats_before["UserCuts: Added ahead-of-time"] == 0
assert stats_before["UserCuts: Added in callback"] > 0 assert stats_before["UserCuts: Added in callback"] > 0

@ -1,6 +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.
import json
import numpy as np import numpy as np
from numpy.linalg import norm from numpy.linalg import norm
@ -66,9 +67,15 @@ def test_subtour() -> None:
samples = instance.get_samples() samples = instance.get_samples()
assert len(samples) == 1 assert len(samples) == 1
sample = samples[0] sample = samples[0]
lazy_enforced = sample.get_array("mip_constr_lazy_enforced")
assert lazy_enforced is not None lazy_encoded = sample.get_scalar("mip_constr_lazy")
assert len(lazy_enforced) > 0 assert lazy_encoded is not None
lazy = json.loads(lazy_encoded)
assert lazy == {
"st[0,1,4]": [0, 1, 4],
"st[2,3,5]": [2, 3, 5],
}
assert_equals( assert_equals(
sample.get_array("mip_var_values"), sample.get_array("mip_var_values"),
[ [

Loading…
Cancel
Save