mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Refactor StaticLazy; remove old constraint methods
This commit is contained in:
@@ -12,7 +12,7 @@ from miplearn.classifiers import Classifier
|
|||||||
from miplearn.classifiers.counting import CountingClassifier
|
from miplearn.classifiers.counting import CountingClassifier
|
||||||
from miplearn.classifiers.threshold import MinProbabilityThreshold, Threshold
|
from miplearn.classifiers.threshold import MinProbabilityThreshold, Threshold
|
||||||
from miplearn.components.component import Component
|
from miplearn.components.component import Component
|
||||||
from miplearn.features import Constraint, Sample
|
from miplearn.features import Constraint, Sample, ConstraintFeatures
|
||||||
from miplearn.instance.base import Instance
|
from miplearn.instance.base import Instance
|
||||||
from miplearn.types import LearningSolveStats
|
from miplearn.types import LearningSolveStats
|
||||||
|
|
||||||
@@ -45,7 +45,8 @@ class StaticLazyConstraintsComponent(Component):
|
|||||||
self.threshold_prototype: Threshold = threshold
|
self.threshold_prototype: Threshold = threshold
|
||||||
self.classifiers: Dict[Hashable, Classifier] = {}
|
self.classifiers: Dict[Hashable, Classifier] = {}
|
||||||
self.thresholds: Dict[Hashable, Threshold] = {}
|
self.thresholds: Dict[Hashable, Threshold] = {}
|
||||||
self.pool: Dict[str, Constraint] = {}
|
self.pool_old: Dict[str, Constraint] = {}
|
||||||
|
self.pool: ConstraintFeatures = ConstraintFeatures()
|
||||||
self.violation_tolerance: float = violation_tolerance
|
self.violation_tolerance: float = violation_tolerance
|
||||||
self.enforced_cids: Set[Hashable] = set()
|
self.enforced_cids: Set[Hashable] = set()
|
||||||
self.n_restored: int = 0
|
self.n_restored: int = 0
|
||||||
@@ -78,24 +79,28 @@ class StaticLazyConstraintsComponent(Component):
|
|||||||
assert solver.internal_solver is not None
|
assert solver.internal_solver is not None
|
||||||
assert sample.after_load is not None
|
assert sample.after_load is not None
|
||||||
assert sample.after_load.instance is not None
|
assert sample.after_load.instance is not None
|
||||||
assert sample.after_load.constraints_old is not None
|
|
||||||
|
|
||||||
logger.info("Predicting violated (static) lazy constraints...")
|
logger.info("Predicting violated (static) lazy constraints...")
|
||||||
if sample.after_load.instance.lazy_constraint_count == 0:
|
if sample.after_load.instance.lazy_constraint_count == 0:
|
||||||
logger.info("Instance does not have static lazy constraints. Skipping.")
|
logger.info("Instance does not have static lazy constraints. Skipping.")
|
||||||
self.enforced_cids = set(self.sample_predict(sample))
|
self.enforced_cids = set(self.sample_predict(sample))
|
||||||
logger.info("Moving lazy constraints to the pool...")
|
logger.info("Moving lazy constraints to the pool...")
|
||||||
self.pool = {}
|
constraints = sample.after_load.constraints
|
||||||
for (cid, cdict) in sample.after_load.constraints_old.items():
|
assert constraints is not None
|
||||||
if cdict.lazy and cid not in self.enforced_cids:
|
assert constraints.lazy is not None
|
||||||
self.pool[cid] = cdict
|
assert constraints.names is not None
|
||||||
solver.internal_solver.remove_constraint(cid)
|
selected = tuple(
|
||||||
logger.info(
|
(constraints.lazy[i] and constraints.names[i] not in self.enforced_cids)
|
||||||
f"{len(self.enforced_cids)} lazy constraints kept; "
|
for i in range(len(constraints.lazy))
|
||||||
f"{len(self.pool)} moved to the pool"
|
|
||||||
)
|
)
|
||||||
stats["LazyStatic: Removed"] = len(self.pool)
|
n_removed = sum(selected)
|
||||||
stats["LazyStatic: Kept"] = len(self.enforced_cids)
|
n_kept = sum(constraints.lazy) - n_removed
|
||||||
|
self.pool = constraints[selected]
|
||||||
|
assert self.pool.names is not None
|
||||||
|
solver.internal_solver.remove_constraints(self.pool.names)
|
||||||
|
logger.info(f"{n_kept} lazy constraints kept; {n_removed} moved to the pool")
|
||||||
|
stats["LazyStatic: Removed"] = n_removed
|
||||||
|
stats["LazyStatic: Kept"] = n_kept
|
||||||
stats["LazyStatic: Restored"] = 0
|
stats["LazyStatic: Restored"] = 0
|
||||||
self.n_restored = 0
|
self.n_restored = 0
|
||||||
self.n_iterations = 0
|
self.n_iterations = 0
|
||||||
@@ -160,25 +165,34 @@ class StaticLazyConstraintsComponent(Component):
|
|||||||
|
|
||||||
def _check_and_add(self, solver: "LearningSolver") -> bool:
|
def _check_and_add(self, solver: "LearningSolver") -> bool:
|
||||||
assert solver.internal_solver is not None
|
assert solver.internal_solver is not None
|
||||||
|
assert self.pool.names is not None
|
||||||
|
if len(self.pool.names) == 0:
|
||||||
|
logger.info("Lazy constraint pool is empty. Skipping violation check.")
|
||||||
|
return False
|
||||||
|
self.n_iterations += 1
|
||||||
logger.info("Finding violated lazy constraints...")
|
logger.info("Finding violated lazy constraints...")
|
||||||
enforced: Dict[str, Constraint] = {}
|
is_satisfied = solver.internal_solver.are_constraints_satisfied(
|
||||||
for (cid, c) in self.pool.items():
|
self.pool,
|
||||||
if not solver.internal_solver.is_constraint_satisfied_old(
|
tol=self.violation_tolerance,
|
||||||
c,
|
|
||||||
tol=self.violation_tolerance,
|
|
||||||
):
|
|
||||||
enforced[cid] = c
|
|
||||||
logger.info(f"{len(enforced)} violations found")
|
|
||||||
for (cid, c) in enforced.items():
|
|
||||||
del self.pool[cid]
|
|
||||||
solver.internal_solver.add_constraint(c, name=cid)
|
|
||||||
self.enforced_cids.add(cid)
|
|
||||||
self.n_restored += 1
|
|
||||||
logger.info(
|
|
||||||
f"{len(enforced)} constraints restored; {len(self.pool)} in the pool"
|
|
||||||
)
|
)
|
||||||
if len(enforced) > 0:
|
is_violated = tuple(not i for i in is_satisfied)
|
||||||
self.n_iterations += 1
|
violated_constraints = self.pool[is_violated]
|
||||||
|
satisfied_constraints = self.pool[is_satisfied]
|
||||||
|
self.pool = satisfied_constraints
|
||||||
|
assert violated_constraints.names is not None
|
||||||
|
assert satisfied_constraints.names is not None
|
||||||
|
n_violated = len(violated_constraints.names)
|
||||||
|
n_satisfied = len(satisfied_constraints.names)
|
||||||
|
logger.info(f"Found {n_violated} violated lazy constraints found")
|
||||||
|
if n_violated > 0:
|
||||||
|
logger.info(
|
||||||
|
"Enforcing {n_violated} lazy constraints; "
|
||||||
|
f"{n_satisfied} left in the pool..."
|
||||||
|
)
|
||||||
|
solver.internal_solver.add_constraints(violated_constraints)
|
||||||
|
for (i, name) in enumerate(violated_constraints.names):
|
||||||
|
self.enforced_cids.add(name)
|
||||||
|
self.n_restored += 1
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -194,12 +208,16 @@ class StaticLazyConstraintsComponent(Component):
|
|||||||
y: Dict[Hashable, List[List[float]]] = {}
|
y: Dict[Hashable, List[List[float]]] = {}
|
||||||
cids: Dict[Hashable, List[str]] = {}
|
cids: Dict[Hashable, List[str]] = {}
|
||||||
assert sample.after_load is not None
|
assert sample.after_load is not None
|
||||||
assert sample.after_load.constraints_old is not None
|
constraints = sample.after_load.constraints
|
||||||
for (cid, constr) in sample.after_load.constraints_old.items():
|
assert constraints is not None
|
||||||
|
assert constraints.names is not None
|
||||||
|
assert constraints.lazy is not None
|
||||||
|
assert constraints.categories is not None
|
||||||
|
for (cidx, cname) in enumerate(constraints.names):
|
||||||
# Initialize categories
|
# Initialize categories
|
||||||
if not constr.lazy:
|
if not constraints.lazy[cidx]:
|
||||||
continue
|
continue
|
||||||
category = constr.category
|
category = constraints.categories[cidx]
|
||||||
if category is None:
|
if category is None:
|
||||||
continue
|
continue
|
||||||
if category not in x:
|
if category not in x:
|
||||||
@@ -212,12 +230,11 @@ class StaticLazyConstraintsComponent(Component):
|
|||||||
if sample.after_lp is not None:
|
if sample.after_lp is not None:
|
||||||
sf = sample.after_lp
|
sf = sample.after_lp
|
||||||
assert sf.instance is not None
|
assert sf.instance is not None
|
||||||
|
assert sf.constraints is not None
|
||||||
features = list(sf.instance.to_list())
|
features = list(sf.instance.to_list())
|
||||||
assert sf.constraints_old is not None
|
features.extend(sf.constraints.to_list(cidx))
|
||||||
assert sf.constraints_old[cid] is not None
|
|
||||||
features.extend(sf.constraints_old[cid].to_list())
|
|
||||||
x[category].append(features)
|
x[category].append(features)
|
||||||
cids[category].append(cid)
|
cids[category].append(cname)
|
||||||
|
|
||||||
# Labels
|
# Labels
|
||||||
if (
|
if (
|
||||||
@@ -225,7 +242,7 @@ class StaticLazyConstraintsComponent(Component):
|
|||||||
and (sample.after_mip.extra is not None)
|
and (sample.after_mip.extra is not None)
|
||||||
and ("lazy_enforced" in sample.after_mip.extra)
|
and ("lazy_enforced" in sample.after_mip.extra)
|
||||||
):
|
):
|
||||||
if cid in sample.after_mip.extra["lazy_enforced"]:
|
if cname in sample.after_mip.extra["lazy_enforced"]:
|
||||||
y[category] += [[False, True]]
|
y[category] += [[False, True]]
|
||||||
else:
|
else:
|
||||||
y[category] += [[True, False]]
|
y[category] += [[True, False]]
|
||||||
|
|||||||
@@ -174,7 +174,6 @@ class Features:
|
|||||||
instance: Optional[InstanceFeatures] = None
|
instance: Optional[InstanceFeatures] = None
|
||||||
variables: Optional[VariableFeatures] = None
|
variables: Optional[VariableFeatures] = None
|
||||||
constraints: Optional[ConstraintFeatures] = None
|
constraints: Optional[ConstraintFeatures] = None
|
||||||
constraints_old: Optional[Dict[str, Constraint]] = None
|
|
||||||
lp_solve: Optional["LPSolveStats"] = None
|
lp_solve: Optional["LPSolveStats"] = None
|
||||||
mip_solve: Optional["MIPSolveStats"] = None
|
mip_solve: Optional["MIPSolveStats"] = None
|
||||||
extra: Optional[Dict] = None
|
extra: Optional[Dict] = None
|
||||||
@@ -212,9 +211,6 @@ class FeaturesExtractor:
|
|||||||
with_sa=self.with_sa,
|
with_sa=self.with_sa,
|
||||||
with_lhs=self.with_lhs,
|
with_lhs=self.with_lhs,
|
||||||
)
|
)
|
||||||
features.constraints_old = solver.get_constraints_old(
|
|
||||||
with_static=with_static,
|
|
||||||
)
|
|
||||||
if with_static:
|
if with_static:
|
||||||
self._extract_user_features_vars(instance, features)
|
self._extract_user_features_vars(instance, features)
|
||||||
self._extract_user_features_constrs(instance, features)
|
self._extract_user_features_constrs(instance, features)
|
||||||
@@ -266,38 +262,50 @@ class FeaturesExtractor:
|
|||||||
instance: "Instance",
|
instance: "Instance",
|
||||||
features: Features,
|
features: Features,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert features.constraints_old is not None
|
assert features.constraints is not None
|
||||||
|
assert features.constraints.names is not None
|
||||||
has_static_lazy = instance.has_static_lazy_constraints()
|
has_static_lazy = instance.has_static_lazy_constraints()
|
||||||
for (cid, constr) in features.constraints_old.items():
|
user_features: List[Optional[Tuple[float, ...]]] = []
|
||||||
user_features = None
|
categories: List[Optional[Hashable]] = []
|
||||||
category = instance.get_constraint_category(cid)
|
lazy: List[bool] = []
|
||||||
|
for (cidx, cname) in enumerate(features.constraints.names):
|
||||||
|
cf: Optional[List[float]] = None
|
||||||
|
category: Optional[Hashable] = instance.get_constraint_category(cname)
|
||||||
if category is not None:
|
if category is not None:
|
||||||
|
categories.append(category)
|
||||||
assert isinstance(category, collections.Hashable), (
|
assert isinstance(category, collections.Hashable), (
|
||||||
f"Constraint category must be hashable. "
|
f"Constraint category must be hashable. "
|
||||||
f"Found {type(category).__name__} instead for cid={cid}.",
|
f"Found {type(category).__name__} instead for cname={cname}.",
|
||||||
)
|
)
|
||||||
user_features = instance.get_constraint_features(cid)
|
cf = instance.get_constraint_features(cname)
|
||||||
if isinstance(user_features, np.ndarray):
|
if isinstance(cf, np.ndarray):
|
||||||
user_features = user_features.tolist()
|
cf = tuple(cf.tolist())
|
||||||
assert isinstance(user_features, list), (
|
assert isinstance(cf, list), (
|
||||||
f"Constraint features must be a list. "
|
f"Constraint features must be a list. "
|
||||||
f"Found {type(user_features).__name__} instead for cid={cid}."
|
f"Found {type(cf).__name__} instead for cname={cname}."
|
||||||
)
|
|
||||||
assert isinstance(user_features[0], float), (
|
|
||||||
f"Constraint features must be a list of floats. "
|
|
||||||
f"Found {type(user_features[0]).__name__} instead for cid={cid}."
|
|
||||||
)
|
)
|
||||||
|
for f in cf:
|
||||||
|
assert isinstance(f, numbers.Real), (
|
||||||
|
f"Constraint features must be a list of numbers. "
|
||||||
|
f"Found {type(f).__name__} instead for cname={cname}."
|
||||||
|
)
|
||||||
|
user_features.append(tuple(cf))
|
||||||
|
else:
|
||||||
|
user_features.append(None)
|
||||||
|
categories.append(None)
|
||||||
if has_static_lazy:
|
if has_static_lazy:
|
||||||
constr.lazy = instance.is_constraint_lazy(cid)
|
lazy.append(instance.is_constraint_lazy(cname))
|
||||||
constr.category = category
|
else:
|
||||||
constr.user_features = user_features
|
lazy.append(False)
|
||||||
|
features.constraints.user_features = tuple(user_features)
|
||||||
|
features.constraints.lazy = tuple(lazy)
|
||||||
|
features.constraints.categories = tuple(categories)
|
||||||
|
|
||||||
def _extract_user_features_instance(
|
def _extract_user_features_instance(
|
||||||
self,
|
self,
|
||||||
instance: "Instance",
|
instance: "Instance",
|
||||||
features: Features,
|
features: Features,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert features.constraints_old is not None
|
|
||||||
user_features = instance.get_instance_features()
|
user_features = instance.get_instance_features()
|
||||||
if isinstance(user_features, np.ndarray):
|
if isinstance(user_features, np.ndarray):
|
||||||
user_features = user_features.tolist()
|
user_features = user_features.tolist()
|
||||||
@@ -310,13 +318,11 @@ class FeaturesExtractor:
|
|||||||
f"Instance features must be a list of numbers. "
|
f"Instance features must be a list of numbers. "
|
||||||
f"Found {type(v).__name__} instead."
|
f"Found {type(v).__name__} instead."
|
||||||
)
|
)
|
||||||
lazy_count = 0
|
assert features.constraints is not None
|
||||||
for (cid, cdict) in features.constraints_old.items():
|
assert features.constraints.lazy is not None
|
||||||
if cdict.lazy:
|
|
||||||
lazy_count += 1
|
|
||||||
features.instance = InstanceFeatures(
|
features.instance = InstanceFeatures(
|
||||||
user_features=user_features,
|
user_features=user_features,
|
||||||
lazy_constraint_count=lazy_count,
|
lazy_constraint_count=sum(features.constraints.lazy),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _extract_alvarez_2017(self, features: Features) -> None:
|
def _extract_alvarez_2017(self, features: Features) -> None:
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ 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)
|
||||||
]
|
]
|
||||||
constr = model.eq_subtour.add(expr=sum(model.x[e] for e in cut_edges) >= 2)
|
constr = model.eq_subtour.add(expr=sum(model.x[e] for e in cut_edges) >= 2)
|
||||||
solver.add_constraint(constr, name="")
|
solver.add_constraint(constr)
|
||||||
|
|
||||||
|
|
||||||
class TravelingSalesmanGenerator:
|
class TravelingSalesmanGenerator:
|
||||||
|
|||||||
@@ -91,26 +91,6 @@ class GurobiSolver(InternalSolver):
|
|||||||
self.gp.GRB.Callback.MIPNODE,
|
self.gp.GRB.Callback.MIPNODE,
|
||||||
]
|
]
|
||||||
|
|
||||||
@overrides
|
|
||||||
def add_constraint(self, constr: Constraint, name: str) -> None:
|
|
||||||
assert self.model is not None
|
|
||||||
assert self._varname_to_var is not None
|
|
||||||
|
|
||||||
assert constr.lhs is not None
|
|
||||||
lhs = self.gp.quicksum(
|
|
||||||
self._varname_to_var[varname] * coeff
|
|
||||||
for (varname, coeff) in constr.lhs.items()
|
|
||||||
)
|
|
||||||
if constr.sense == "=":
|
|
||||||
self.model.addConstr(lhs == constr.rhs, name=name)
|
|
||||||
elif constr.sense == "<":
|
|
||||||
self.model.addConstr(lhs <= constr.rhs, name=name)
|
|
||||||
else:
|
|
||||||
self.model.addConstr(lhs >= constr.rhs, name=name)
|
|
||||||
self._dirty = True
|
|
||||||
self._has_lp_solution = False
|
|
||||||
self._has_mip_solution = False
|
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def add_constraints(self, cf: ConstraintFeatures) -> None:
|
def add_constraints(self, cf: ConstraintFeatures) -> None:
|
||||||
assert cf.names is not None
|
assert cf.names is not None
|
||||||
@@ -143,7 +123,7 @@ class GurobiSolver(InternalSolver):
|
|||||||
self,
|
self,
|
||||||
cf: ConstraintFeatures,
|
cf: ConstraintFeatures,
|
||||||
tol: float = 1e-5,
|
tol: float = 1e-5,
|
||||||
) -> List[bool]:
|
) -> Tuple[bool, ...]:
|
||||||
assert cf.names is not None
|
assert cf.names is not None
|
||||||
assert cf.senses is not None
|
assert cf.senses is not None
|
||||||
assert cf.lhs is not None
|
assert cf.lhs is not None
|
||||||
@@ -162,7 +142,7 @@ class GurobiSolver(InternalSolver):
|
|||||||
result.append(lhs >= cf.rhs[i] - tol)
|
result.append(lhs >= cf.rhs[i] - tol)
|
||||||
else:
|
else:
|
||||||
result.append(abs(cf.rhs[i] - lhs) <= tol)
|
result.append(abs(cf.rhs[i] - lhs) <= tol)
|
||||||
return result
|
return tuple(result)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def build_test_instance_infeasible(self) -> Instance:
|
def build_test_instance_infeasible(self) -> Instance:
|
||||||
@@ -289,76 +269,6 @@ class GurobiSolver(InternalSolver):
|
|||||||
slacks=slacks,
|
slacks=slacks,
|
||||||
)
|
)
|
||||||
|
|
||||||
@overrides
|
|
||||||
def get_constraints_old(self, with_static: bool = True) -> Dict[str, Constraint]:
|
|
||||||
model = self.model
|
|
||||||
assert model is not None
|
|
||||||
self._raise_if_callback()
|
|
||||||
if self._dirty:
|
|
||||||
model.update()
|
|
||||||
self._dirty = False
|
|
||||||
|
|
||||||
gp_constrs = model.getConstrs()
|
|
||||||
constr_names = model.getAttr("constrName", gp_constrs)
|
|
||||||
lhs: Optional[List[Dict]] = None
|
|
||||||
rhs = None
|
|
||||||
sense = None
|
|
||||||
dual_value = None
|
|
||||||
sa_rhs_up = None
|
|
||||||
sa_rhs_down = None
|
|
||||||
slack = None
|
|
||||||
basis_status = None
|
|
||||||
|
|
||||||
if with_static:
|
|
||||||
var_names = model.getAttr("varName", model.getVars())
|
|
||||||
rhs = model.getAttr("rhs", gp_constrs)
|
|
||||||
sense = model.getAttr("sense", gp_constrs)
|
|
||||||
lhs = []
|
|
||||||
for (i, gp_constr) in enumerate(gp_constrs):
|
|
||||||
expr = model.getRow(gp_constr)
|
|
||||||
lhsi = {}
|
|
||||||
for j in range(expr.size()):
|
|
||||||
lhsi[var_names[expr.getVar(j).index]] = expr.getCoeff(j)
|
|
||||||
lhs.append(lhsi)
|
|
||||||
if self._has_lp_solution:
|
|
||||||
dual_value = model.getAttr("pi", gp_constrs)
|
|
||||||
sa_rhs_up = model.getAttr("saRhsUp", gp_constrs)
|
|
||||||
sa_rhs_down = model.getAttr("saRhsLow", gp_constrs)
|
|
||||||
basis_status = model.getAttr("cbasis", gp_constrs)
|
|
||||||
if self._has_lp_solution or self._has_mip_solution:
|
|
||||||
slack = model.getAttr("slack", gp_constrs)
|
|
||||||
|
|
||||||
constraints: Dict[str, Constraint] = {}
|
|
||||||
for (i, gp_constr) in enumerate(gp_constrs):
|
|
||||||
assert (
|
|
||||||
constr_names[i] not in constraints
|
|
||||||
), f"Duplicated constraint name detected: {constr_names[i]}"
|
|
||||||
constraint = Constraint()
|
|
||||||
if with_static:
|
|
||||||
assert lhs is not None
|
|
||||||
assert rhs is not None
|
|
||||||
assert sense is not None
|
|
||||||
constraint.lhs = lhs[i]
|
|
||||||
constraint.rhs = rhs[i]
|
|
||||||
constraint.sense = sense[i]
|
|
||||||
if dual_value is not None:
|
|
||||||
assert sa_rhs_up is not None
|
|
||||||
assert sa_rhs_down is not None
|
|
||||||
assert basis_status is not None
|
|
||||||
constraint.dual_value = dual_value[i]
|
|
||||||
constraint.sa_rhs_up = sa_rhs_up[i]
|
|
||||||
constraint.sa_rhs_down = sa_rhs_down[i]
|
|
||||||
if gp_constr.cbasis == 0:
|
|
||||||
constraint.basis_status = "B"
|
|
||||||
elif gp_constr.cbasis == -1:
|
|
||||||
constraint.basis_status = "N"
|
|
||||||
else:
|
|
||||||
raise Exception(f"unknown cbasis: {gp_constr.cbasis}")
|
|
||||||
if slack is not None:
|
|
||||||
constraint.slack = slack[i]
|
|
||||||
constraints[constr_names[i]] = constraint
|
|
||||||
return constraints
|
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def get_solution(self) -> Optional[Solution]:
|
def get_solution(self) -> Optional[Solution]:
|
||||||
assert self.model is not None
|
assert self.model is not None
|
||||||
@@ -470,42 +380,6 @@ class GurobiSolver(InternalSolver):
|
|||||||
values=values,
|
values=values,
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_constraint_satisfied(
|
|
||||||
self,
|
|
||||||
names: List[str],
|
|
||||||
tol: float = 1e-6,
|
|
||||||
) -> List[bool]:
|
|
||||||
def _check(c: Tuple) -> bool:
|
|
||||||
lhs, sense, rhs = c
|
|
||||||
lhs_value = lhs.getValue()
|
|
||||||
if sense == "=":
|
|
||||||
return abs(lhs_value - rhs) < tol
|
|
||||||
elif sense == ">":
|
|
||||||
return lhs_value > rhs - tol
|
|
||||||
else:
|
|
||||||
return lhs_value < rhs - tol
|
|
||||||
|
|
||||||
constrs = [self._relaxed_constrs[n] for n in names]
|
|
||||||
return list(map(_check, constrs))
|
|
||||||
|
|
||||||
@overrides
|
|
||||||
def is_constraint_satisfied_old(
|
|
||||||
self,
|
|
||||||
constr: Constraint,
|
|
||||||
tol: float = 1e-6,
|
|
||||||
) -> bool:
|
|
||||||
assert constr.lhs is not None
|
|
||||||
lhs = 0.0
|
|
||||||
for (varname, coeff) in constr.lhs.items():
|
|
||||||
var = self._varname_to_var[varname]
|
|
||||||
lhs += self._get_value(var) * coeff
|
|
||||||
if constr.sense == "<":
|
|
||||||
return lhs <= constr.rhs + tol
|
|
||||||
elif constr.sense == ">":
|
|
||||||
return lhs >= constr.rhs - tol
|
|
||||||
else:
|
|
||||||
return abs(constr.rhs - lhs) < abs(tol)
|
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def is_infeasible(self) -> bool:
|
def is_infeasible(self) -> bool:
|
||||||
assert self.model is not None
|
assert self.model is not None
|
||||||
@@ -521,13 +395,7 @@ class GurobiSolver(InternalSolver):
|
|||||||
self.model.update()
|
self.model.update()
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def remove_constraint(self, name: str) -> None:
|
def remove_constraints(self, names: Tuple[str, ...]) -> None:
|
||||||
assert self.model is not None
|
|
||||||
constr = self.model.getConstrByName(name)
|
|
||||||
self.model.remove(constr)
|
|
||||||
|
|
||||||
@overrides
|
|
||||||
def remove_constraints(self, names: List[str]) -> None:
|
|
||||||
assert self.model is not None
|
assert self.model is not None
|
||||||
constrs = [self.model.getConstrByName(n) for n in names]
|
constrs = [self.model.getConstrByName(n) for n in names]
|
||||||
self.model.remove(constrs)
|
self.model.remove(constrs)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from miplearn.features import Constraint, VariableFeatures, ConstraintFeatures
|
from miplearn.features import Constraint, VariableFeatures, ConstraintFeatures
|
||||||
from miplearn.instance.base import Instance
|
from miplearn.instance.base import Instance
|
||||||
@@ -51,21 +51,185 @@ class InternalSolver(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def solve_lp(
|
def add_constraints(self, cf: ConstraintFeatures) -> None:
|
||||||
self,
|
"""Adds the given constraints to the model."""
|
||||||
tee: bool = False,
|
pass
|
||||||
) -> LPSolveStats:
|
|
||||||
"""
|
|
||||||
Solves the LP relaxation of the currently loaded instance. After this
|
|
||||||
method finishes, the solution can be retrieved by calling `get_solution`.
|
|
||||||
|
|
||||||
This method should not permanently modify the problem. That is, subsequent
|
@abstractmethod
|
||||||
calls to `solve` should solve the original MIP, not the LP relaxation.
|
def are_constraints_satisfied(
|
||||||
|
self,
|
||||||
|
cf: ConstraintFeatures,
|
||||||
|
tol: float = 1e-5,
|
||||||
|
) -> Tuple[bool, ...]:
|
||||||
|
"""
|
||||||
|
Checks whether the current solution satisfies the given constraints.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def are_callbacks_supported(self) -> bool:
|
||||||
|
"""
|
||||||
|
Returns True if this solver supports native callbacks, such as lazy constraints
|
||||||
|
callback or user cuts callback.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def build_test_instance_infeasible(self) -> Instance:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def build_test_instance_knapsack(self) -> Instance:
|
||||||
|
"""
|
||||||
|
Returns an instance corresponding to the following MIP, for testing purposes:
|
||||||
|
|
||||||
|
maximize 505 x0 + 352 x1 + 458 x2 + 220 x3
|
||||||
|
s.t. eq_capacity: z = 23 x0 + 26 x1 + 20 x2 + 18 x3
|
||||||
|
x0, x1, x2, x3 binary
|
||||||
|
0 <= z <= 67 continuous
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def build_test_instance_redundancy(self) -> Instance:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def clone(self) -> "InternalSolver":
|
||||||
|
"""
|
||||||
|
Returns a new copy of this solver with identical parameters, but otherwise
|
||||||
|
completely unitialized.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def fix(self, solution: Solution) -> None:
|
||||||
|
"""
|
||||||
|
Fixes the values of a subset of decision variables. Missing values in the
|
||||||
|
solution indicate variables that should be left free.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_solution(self) -> Optional[Solution]:
|
||||||
|
"""
|
||||||
|
Returns current solution found by the solver.
|
||||||
|
|
||||||
|
If called after `solve`, returns the best primal solution found during
|
||||||
|
the search. If called after `solve_lp`, returns the optimal solution
|
||||||
|
to the LP relaxation. If no primal solution is available, return None.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_constraint_attrs(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Returns a list of constraint attributes supported by this solver. Used for
|
||||||
|
testing purposes only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_constraints(
|
||||||
|
self,
|
||||||
|
with_static: bool = True,
|
||||||
|
with_sa: bool = True,
|
||||||
|
with_lhs: bool = True,
|
||||||
|
) -> ConstraintFeatures:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_variable_attrs(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Returns a list of variable attributes supported by this solver. Used for
|
||||||
|
testing purposes only.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_variables(
|
||||||
|
self,
|
||||||
|
with_static: bool = True,
|
||||||
|
with_sa: bool = True,
|
||||||
|
) -> VariableFeatures:
|
||||||
|
"""
|
||||||
|
Returns a description of the decision variables in the problem.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
tee
|
with_static: bool
|
||||||
If true, prints the solver log to the screen.
|
If True, include features that do not change during the solution process,
|
||||||
|
such as variable types and names. This parameter is used to reduce the
|
||||||
|
amount of duplicated data collected by LearningSolver. Features that do
|
||||||
|
not change are only collected once.
|
||||||
|
with_sa: bool
|
||||||
|
If True, collect sensitivity analysis information. For large models,
|
||||||
|
collecting this information may be expensive, so this parameter is useful
|
||||||
|
for reducing running times.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_infeasible(self) -> bool:
|
||||||
|
"""
|
||||||
|
Returns True if the model has been proved to be infeasible.
|
||||||
|
Must be called after solve.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_constraints(self, names: Tuple[str, ...]) -> None:
|
||||||
|
"""
|
||||||
|
Removes the given constraints from the model.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def relax(self) -> None:
|
||||||
|
"""
|
||||||
|
Drops all integrality constraints from the model.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_branching_priorities(self, priorities: BranchPriorities) -> None:
|
||||||
|
"""
|
||||||
|
Sets the branching priorities for the given decision variables.
|
||||||
|
|
||||||
|
When the MIP solver needs to decide on which variable to branch, variables
|
||||||
|
with higher priority are picked first, given that they are fractional.
|
||||||
|
Ties are solved arbitrarily. By default, all variables have priority zero.
|
||||||
|
|
||||||
|
Missing values indicate variables whose priorities should not be modified.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_instance(
|
||||||
|
self,
|
||||||
|
instance: Instance,
|
||||||
|
model: Any = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Loads the given instance into the solver.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
instance: Instance
|
||||||
|
The instance to be loaded.
|
||||||
|
model: Any
|
||||||
|
The concrete optimization model corresponding to this instance
|
||||||
|
(e.g. JuMP.Model or pyomo.core.ConcreteModel). If not provided,
|
||||||
|
it will be generated by calling `instance.to_model()`.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_warm_start(self, solution: Solution) -> None:
|
||||||
|
"""
|
||||||
|
Sets the warm start to be used by the solver.
|
||||||
|
|
||||||
|
Only one warm start is supported. Calling this function when a warm start
|
||||||
|
already exists will remove the previous warm start.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -106,211 +270,20 @@ class InternalSolver(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_solution(self) -> Optional[Solution]:
|
def solve_lp(
|
||||||
"""
|
|
||||||
Returns current solution found by the solver.
|
|
||||||
|
|
||||||
If called after `solve`, returns the best primal solution found during
|
|
||||||
the search. If called after `solve_lp`, returns the optimal solution
|
|
||||||
to the LP relaxation. If no primal solution is available, return None.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def set_warm_start(self, solution: Solution) -> None:
|
|
||||||
"""
|
|
||||||
Sets the warm start to be used by the solver.
|
|
||||||
|
|
||||||
Only one warm start is supported. Calling this function when a warm start
|
|
||||||
already exists will remove the previous warm start.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def set_instance(
|
|
||||||
self,
|
self,
|
||||||
instance: Instance,
|
tee: bool = False,
|
||||||
model: Any = None,
|
) -> LPSolveStats:
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Loads the given instance into the solver.
|
Solves the LP relaxation of the currently loaded instance. After this
|
||||||
|
method finishes, the solution can be retrieved by calling `get_solution`.
|
||||||
|
|
||||||
|
This method should not permanently modify the problem. That is, subsequent
|
||||||
|
calls to `solve` should solve the original MIP, not the LP relaxation.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
instance: Instance
|
tee
|
||||||
The instance to be loaded.
|
If true, prints the solver log to the screen.
|
||||||
model: Any
|
|
||||||
The concrete optimization model corresponding to this instance
|
|
||||||
(e.g. JuMP.Model or pyomo.core.ConcreteModel). If not provided,
|
|
||||||
it will be generated by calling `instance.to_model()`.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def fix(self, solution: Solution) -> None:
|
|
||||||
"""
|
|
||||||
Fixes the values of a subset of decision variables. Missing values in the
|
|
||||||
solution indicate variables that should be left free.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_branching_priorities(self, priorities: BranchPriorities) -> None:
|
|
||||||
"""
|
|
||||||
Sets the branching priorities for the given decision variables.
|
|
||||||
|
|
||||||
When the MIP solver needs to decide on which variable to branch, variables
|
|
||||||
with higher priority are picked first, given that they are fractional.
|
|
||||||
Ties are solved arbitrarily. By default, all variables have priority zero.
|
|
||||||
|
|
||||||
Missing values indicate variables whose priorities should not be modified.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_constraints(
|
|
||||||
self,
|
|
||||||
with_static: bool = True,
|
|
||||||
with_sa: bool = True,
|
|
||||||
with_lhs: bool = True,
|
|
||||||
) -> ConstraintFeatures:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_constraints_old(self, with_static: bool = True) -> Dict[str, Constraint]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def add_constraint(self, constr: Constraint, name: str) -> None:
|
|
||||||
"""
|
|
||||||
Adds a given constraint to the model.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def add_constraints(self, cf: ConstraintFeatures) -> None:
|
|
||||||
"""Adds the given constraints to the model."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def are_constraints_satisfied(
|
|
||||||
self,
|
|
||||||
cf: ConstraintFeatures,
|
|
||||||
tol: float = 1e-5,
|
|
||||||
) -> List[bool]:
|
|
||||||
"""
|
|
||||||
Checks whether the current solution satisfies the given constraints.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def remove_constraint(self, name: str) -> None:
|
|
||||||
"""
|
|
||||||
Removes the constraint that has a given name from the model.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def remove_constraints(self, names: List[str]) -> None:
|
|
||||||
"""
|
|
||||||
Removes the given constraints from the model.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def is_constraint_satisfied_old(
|
|
||||||
self, constr: Constraint, tol: float = 1e-6
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Returns True if the current solution satisfies the given constraint.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def relax(self) -> None:
|
|
||||||
"""
|
|
||||||
Drops all integrality constraints from the model.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def is_infeasible(self) -> bool:
|
|
||||||
"""
|
|
||||||
Returns True if the model has been proved to be infeasible.
|
|
||||||
Must be called after solve.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def clone(self) -> "InternalSolver":
|
|
||||||
"""
|
|
||||||
Returns a new copy of this solver with identical parameters, but otherwise
|
|
||||||
completely unitialized.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def build_test_instance_infeasible(self) -> Instance:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def build_test_instance_redundancy(self) -> Instance:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def build_test_instance_knapsack(self) -> Instance:
|
|
||||||
"""
|
|
||||||
Returns an instance corresponding to the following MIP, for testing purposes:
|
|
||||||
|
|
||||||
maximize 505 x0 + 352 x1 + 458 x2 + 220 x3
|
|
||||||
s.t. eq_capacity: z = 23 x0 + 26 x1 + 20 x2 + 18 x3
|
|
||||||
x0, x1, x2, x3 binary
|
|
||||||
0 <= z <= 67 continuous
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def are_callbacks_supported(self) -> bool:
|
|
||||||
"""
|
|
||||||
Returns True if this solver supports native callbacks, such as lazy constraints
|
|
||||||
callback or user cuts callback.
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_variables(
|
|
||||||
self,
|
|
||||||
with_static: bool = True,
|
|
||||||
with_sa: bool = True,
|
|
||||||
) -> VariableFeatures:
|
|
||||||
"""
|
|
||||||
Returns a description of the decision variables in the problem.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
with_static: bool
|
|
||||||
If True, include features that do not change during the solution process,
|
|
||||||
such as variable types and names. This parameter is used to reduce the
|
|
||||||
amount of duplicated data collected by LearningSolver. Features that do
|
|
||||||
not change are only collected once.
|
|
||||||
with_sa: bool
|
|
||||||
If True, collect sensitivity analysis information. For large models,
|
|
||||||
collecting this information may be expensive, so this parameter is useful
|
|
||||||
for reducing running times.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_constraint_attrs(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
Returns a list of constraint attributes supported by this solver. Used for
|
|
||||||
testing purposes only.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_variable_attrs(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
Returns a list of variable attributes supported by this solver. Used for
|
|
||||||
testing purposes only.
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from miplearn.solvers.internal import (
|
|||||||
IterationCallback,
|
IterationCallback,
|
||||||
LazyCallback,
|
LazyCallback,
|
||||||
MIPSolveStats,
|
MIPSolveStats,
|
||||||
Constraint,
|
|
||||||
)
|
)
|
||||||
from miplearn.types import (
|
from miplearn.types import (
|
||||||
SolverParams,
|
SolverParams,
|
||||||
@@ -69,31 +68,12 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
for (key, value) in params.items():
|
for (key, value) in params.items():
|
||||||
self._pyomo_solver.options[key] = value
|
self._pyomo_solver.options[key] = value
|
||||||
|
|
||||||
@overrides
|
|
||||||
def add_constraint(
|
def add_constraint(
|
||||||
self,
|
self,
|
||||||
constr: Any,
|
constr: Any,
|
||||||
name: str,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
assert self.model is not None
|
assert self.model is not None
|
||||||
if isinstance(constr, Constraint):
|
self._pyomo_solver.add_constraint(constr)
|
||||||
assert constr.lhs is not None
|
|
||||||
lhs = 0.0
|
|
||||||
for (varname, coeff) in constr.lhs.items():
|
|
||||||
var = self._varname_to_var[varname]
|
|
||||||
lhs += var * coeff
|
|
||||||
if constr.sense == "=":
|
|
||||||
expr = lhs == constr.rhs
|
|
||||||
elif constr.sense == "<":
|
|
||||||
expr = lhs <= constr.rhs
|
|
||||||
else:
|
|
||||||
expr = lhs >= constr.rhs
|
|
||||||
cl = pe.Constraint(expr=expr, name=name)
|
|
||||||
self.model.add_component(name, cl)
|
|
||||||
self._pyomo_solver.add_constraint(cl)
|
|
||||||
self._cname_to_constr[name] = cl
|
|
||||||
else:
|
|
||||||
self._pyomo_solver.add_constraint(constr)
|
|
||||||
self._termination_condition = ""
|
self._termination_condition = ""
|
||||||
self._has_lp_solution = False
|
self._has_lp_solution = False
|
||||||
self._has_mip_solution = False
|
self._has_mip_solution = False
|
||||||
@@ -133,7 +113,7 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
self,
|
self,
|
||||||
cf: ConstraintFeatures,
|
cf: ConstraintFeatures,
|
||||||
tol: float = 1e-5,
|
tol: float = 1e-5,
|
||||||
) -> List[bool]:
|
) -> Tuple[bool, ...]:
|
||||||
assert cf.names is not None
|
assert cf.names is not None
|
||||||
assert cf.lhs is not None
|
assert cf.lhs is not None
|
||||||
assert cf.rhs is not None
|
assert cf.rhs is not None
|
||||||
@@ -150,7 +130,7 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
result.append(lhs >= cf.rhs[i] - tol)
|
result.append(lhs >= cf.rhs[i] - tol)
|
||||||
else:
|
else:
|
||||||
result.append(abs(cf.rhs[i] - lhs) < tol)
|
result.append(abs(cf.rhs[i] - lhs) < tol)
|
||||||
return result
|
return tuple(result)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def build_test_instance_infeasible(self) -> Instance:
|
def build_test_instance_infeasible(self) -> Instance:
|
||||||
@@ -277,30 +257,6 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
dual_values=dual_values_t,
|
dual_values=dual_values_t,
|
||||||
)
|
)
|
||||||
|
|
||||||
@overrides
|
|
||||||
def get_constraints_old(self, with_static: bool = True) -> Dict[str, Constraint]:
|
|
||||||
assert self.model is not None
|
|
||||||
|
|
||||||
constraints = {}
|
|
||||||
for constr in self.model.component_objects(pyomo.core.Constraint):
|
|
||||||
if isinstance(constr, pe.ConstraintList):
|
|
||||||
for idx in constr:
|
|
||||||
name = f"{constr.name}[{idx}]"
|
|
||||||
assert name not in constraints
|
|
||||||
constraints[name] = self._parse_pyomo_constraint(
|
|
||||||
constr[idx],
|
|
||||||
with_static=with_static,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
name = constr.name
|
|
||||||
assert name not in constraints
|
|
||||||
constraints[name] = self._parse_pyomo_constraint(
|
|
||||||
constr,
|
|
||||||
with_static=with_static,
|
|
||||||
)
|
|
||||||
|
|
||||||
return constraints
|
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def get_constraint_attrs(self) -> List[str]:
|
def get_constraint_attrs(self) -> List[str]:
|
||||||
return [
|
return [
|
||||||
@@ -435,36 +391,12 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
"values",
|
"values",
|
||||||
]
|
]
|
||||||
|
|
||||||
@overrides
|
|
||||||
def is_constraint_satisfied_old(
|
|
||||||
self, constr: Constraint, tol: float = 1e-6
|
|
||||||
) -> bool:
|
|
||||||
lhs = 0.0
|
|
||||||
assert constr.lhs is not None
|
|
||||||
for (varname, coeff) in constr.lhs.items():
|
|
||||||
var = self._varname_to_var[varname]
|
|
||||||
lhs += var.value * coeff
|
|
||||||
if constr.sense == "<":
|
|
||||||
return lhs <= constr.rhs + tol
|
|
||||||
elif constr.sense == ">":
|
|
||||||
return lhs >= constr.rhs - tol
|
|
||||||
else:
|
|
||||||
return abs(constr.rhs - lhs) < abs(tol)
|
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def is_infeasible(self) -> bool:
|
def is_infeasible(self) -> bool:
|
||||||
return self._termination_condition == TerminationCondition.infeasible
|
return self._termination_condition == TerminationCondition.infeasible
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def remove_constraint(self, name: str) -> None:
|
def remove_constraints(self, names: Tuple[str, ...]) -> None:
|
||||||
assert self.model is not None
|
|
||||||
constr = self._cname_to_constr[name]
|
|
||||||
del self._cname_to_constr[name]
|
|
||||||
self.model.del_component(constr)
|
|
||||||
self._pyomo_solver.remove_constraint(constr)
|
|
||||||
|
|
||||||
@overrides
|
|
||||||
def remove_constraints(self, names: List[str]) -> None:
|
|
||||||
assert self.model is not None
|
assert self.model is not None
|
||||||
for name in names:
|
for name in names:
|
||||||
constr = self._cname_to_constr[name]
|
constr = self._cname_to_constr[name]
|
||||||
@@ -627,46 +559,6 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
def _get_warm_start_regexp(self) -> Optional[str]:
|
def _get_warm_start_regexp(self) -> Optional[str]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _parse_pyomo_constraint(
|
|
||||||
self,
|
|
||||||
pyomo_constr: pyomo.core.Constraint,
|
|
||||||
with_static: bool = True,
|
|
||||||
) -> Constraint:
|
|
||||||
assert self.model is not None
|
|
||||||
constr = Constraint()
|
|
||||||
|
|
||||||
if with_static:
|
|
||||||
# Extract RHS and sense
|
|
||||||
has_ub = pyomo_constr.has_ub()
|
|
||||||
has_lb = pyomo_constr.has_lb()
|
|
||||||
assert (
|
|
||||||
(not has_lb)
|
|
||||||
or (not has_ub)
|
|
||||||
or pyomo_constr.upper() == pyomo_constr.lower()
|
|
||||||
), "range constraints not supported"
|
|
||||||
if not has_ub:
|
|
||||||
constr.sense = ">"
|
|
||||||
constr.rhs = pyomo_constr.lower()
|
|
||||||
elif not has_lb:
|
|
||||||
constr.sense = "<"
|
|
||||||
constr.rhs = pyomo_constr.upper()
|
|
||||||
else:
|
|
||||||
constr.sense = "="
|
|
||||||
constr.rhs = pyomo_constr.upper()
|
|
||||||
|
|
||||||
# Extract LHS
|
|
||||||
constr.lhs = self._parse_pyomo_expr(pyomo_constr.body)
|
|
||||||
|
|
||||||
# Extract solution attributes
|
|
||||||
if self._has_lp_solution:
|
|
||||||
constr.dual_value = self.model.dual[pyomo_constr]
|
|
||||||
|
|
||||||
if self._has_mip_solution or self._has_lp_solution:
|
|
||||||
constr.slack = self.model.slack[pyomo_constr]
|
|
||||||
|
|
||||||
# Build constraint
|
|
||||||
return constr
|
|
||||||
|
|
||||||
def _parse_pyomo_expr(self, expr: Any) -> Dict[str, float]:
|
def _parse_pyomo_expr(self, expr: Any) -> Dict[str, float]:
|
||||||
lhs = {}
|
lhs = {}
|
||||||
if isinstance(expr, SumExpression):
|
if isinstance(expr, SumExpression):
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
|||||||
rhs=(0.0,),
|
rhs=(0.0,),
|
||||||
senses=("<",),
|
senses=("<",),
|
||||||
)
|
)
|
||||||
assert_equals(solver.are_constraints_satisfied(cf), [False])
|
assert_equals(solver.are_constraints_satisfied(cf), (False,))
|
||||||
|
|
||||||
# Add constraint and verify it affects solution
|
# Add constraint and verify it affects solution
|
||||||
solver.add_constraints(cf)
|
solver.add_constraints(cf)
|
||||||
@@ -227,10 +227,10 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
|||||||
)
|
)
|
||||||
stats = solver.solve()
|
stats = solver.solve()
|
||||||
assert_equals(stats.mip_lower_bound, 1030.0)
|
assert_equals(stats.mip_lower_bound, 1030.0)
|
||||||
assert_equals(solver.are_constraints_satisfied(cf), [True])
|
assert_equals(solver.are_constraints_satisfied(cf), (True,))
|
||||||
|
|
||||||
# Remove the new constraint
|
# Remove the new constraint
|
||||||
solver.remove_constraints(["cut"])
|
solver.remove_constraints(("cut",))
|
||||||
|
|
||||||
# New constraint should no longer affect solution
|
# New constraint should no longer affect solution
|
||||||
stats = solver.solve()
|
stats = solver.solve()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from miplearn.features import (
|
|||||||
Features,
|
Features,
|
||||||
Constraint,
|
Constraint,
|
||||||
Sample,
|
Sample,
|
||||||
|
ConstraintFeatures,
|
||||||
)
|
)
|
||||||
from miplearn.instance.base import Instance
|
from miplearn.instance.base import Instance
|
||||||
from miplearn.solvers.internal import InternalSolver
|
from miplearn.solvers.internal import InternalSolver
|
||||||
@@ -32,23 +33,21 @@ def sample() -> Sample:
|
|||||||
instance=InstanceFeatures(
|
instance=InstanceFeatures(
|
||||||
lazy_constraint_count=4,
|
lazy_constraint_count=4,
|
||||||
),
|
),
|
||||||
constraints_old={
|
constraints=ConstraintFeatures(
|
||||||
"c1": Constraint(category="type-a", lazy=True),
|
names=("c1", "c2", "c3", "c4", "c5"),
|
||||||
"c2": Constraint(category="type-a", lazy=True),
|
categories=(
|
||||||
"c3": Constraint(category="type-a", lazy=True),
|
"type-a",
|
||||||
"c4": Constraint(category="type-b", lazy=True),
|
"type-a",
|
||||||
"c5": Constraint(category="type-b", lazy=False),
|
"type-a",
|
||||||
},
|
"type-b",
|
||||||
|
"type-b",
|
||||||
|
),
|
||||||
|
lazy=(True, True, True, True, False),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
after_lp=Features(
|
after_lp=Features(
|
||||||
instance=InstanceFeatures(),
|
instance=InstanceFeatures(),
|
||||||
constraints_old={
|
constraints=ConstraintFeatures(names=("c1", "c2", "c3", "c4", "c5")),
|
||||||
"c1": Constraint(),
|
|
||||||
"c2": Constraint(),
|
|
||||||
"c3": Constraint(),
|
|
||||||
"c4": Constraint(),
|
|
||||||
"c5": Constraint(),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
after_mip=Features(
|
after_mip=Features(
|
||||||
extra={
|
extra={
|
||||||
@@ -57,17 +56,14 @@ def sample() -> Sample:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
sample.after_lp.instance.to_list = Mock(return_value=[5.0]) # type: ignore
|
sample.after_lp.instance.to_list = Mock(return_value=[5.0]) # type: ignore
|
||||||
sample.after_lp.constraints_old["c1"].to_list = Mock( # type: ignore
|
sample.after_lp.constraints.to_list = Mock( # type: ignore
|
||||||
return_value=[1.0, 1.0]
|
side_effect=lambda idx: {
|
||||||
)
|
0: [1.0, 1.0],
|
||||||
sample.after_lp.constraints_old["c2"].to_list = Mock( # type: ignore
|
1: [1.0, 2.0],
|
||||||
return_value=[1.0, 2.0]
|
2: [1.0, 3.0],
|
||||||
)
|
3: [1.0, 4.0, 0.0],
|
||||||
sample.after_lp.constraints_old["c3"].to_list = Mock( # type: ignore
|
4: None,
|
||||||
return_value=[1.0, 3.0]
|
}[idx]
|
||||||
)
|
|
||||||
sample.after_lp.constraints_old["c4"].to_list = Mock( # type: ignore
|
|
||||||
return_value=[1.0, 4.0, 0.0]
|
|
||||||
)
|
)
|
||||||
return sample
|
return sample
|
||||||
|
|
||||||
@@ -87,6 +83,9 @@ def test_usage_with_solver(instance: Instance) -> None:
|
|||||||
|
|
||||||
internal = solver.internal_solver = Mock(spec=InternalSolver)
|
internal = solver.internal_solver = Mock(spec=InternalSolver)
|
||||||
internal.is_constraint_satisfied_old = Mock(return_value=False)
|
internal.is_constraint_satisfied_old = Mock(return_value=False)
|
||||||
|
internal.are_constraints_satisfied = Mock(
|
||||||
|
side_effect=lambda cf, tol=1.0: [False for i in range(len(cf.names))]
|
||||||
|
)
|
||||||
|
|
||||||
component = StaticLazyConstraintsComponent(violation_tolerance=1.0)
|
component = StaticLazyConstraintsComponent(violation_tolerance=1.0)
|
||||||
component.thresholds["type-a"] = MinProbabilityThreshold([0.5, 0.5])
|
component.thresholds["type-a"] = MinProbabilityThreshold([0.5, 0.5])
|
||||||
@@ -115,7 +114,6 @@ def test_usage_with_solver(instance: Instance) -> None:
|
|||||||
stats: LearningSolveStats = {}
|
stats: LearningSolveStats = {}
|
||||||
sample = instance.samples[0]
|
sample = instance.samples[0]
|
||||||
assert sample.after_load is not None
|
assert sample.after_load is not None
|
||||||
assert sample.after_load.constraints_old is not None
|
|
||||||
assert sample.after_mip is not None
|
assert sample.after_mip is not None
|
||||||
assert sample.after_mip.extra is not None
|
assert sample.after_mip.extra is not None
|
||||||
del sample.after_mip.extra["lazy_enforced"]
|
del sample.after_mip.extra["lazy_enforced"]
|
||||||
@@ -134,8 +132,8 @@ def test_usage_with_solver(instance: Instance) -> None:
|
|||||||
component.classifiers["type-b"].predict_proba.assert_called_once()
|
component.classifiers["type-b"].predict_proba.assert_called_once()
|
||||||
|
|
||||||
# Should ask internal solver to remove some constraints
|
# Should ask internal solver to remove some constraints
|
||||||
assert internal.remove_constraint.call_count == 1
|
assert internal.remove_constraints.call_count == 1
|
||||||
internal.remove_constraint.assert_has_calls([call("c3")])
|
internal.remove_constraints.assert_has_calls([call(("c3",))])
|
||||||
|
|
||||||
# LearningSolver calls after_iteration (first time)
|
# LearningSolver calls after_iteration (first time)
|
||||||
should_repeat = component.iteration_cb(solver, instance, None)
|
should_repeat = component.iteration_cb(solver, instance, None)
|
||||||
@@ -143,19 +141,20 @@ def test_usage_with_solver(instance: Instance) -> None:
|
|||||||
|
|
||||||
# Should ask internal solver to verify if constraints in the pool are
|
# Should ask internal solver to verify if constraints in the pool are
|
||||||
# satisfied and add the ones that are not
|
# satisfied and add the ones that are not
|
||||||
c3 = sample.after_load.constraints_old["c3"]
|
assert sample.after_load.constraints is not None
|
||||||
internal.is_constraint_satisfied_old.assert_called_once_with(c3, tol=1.0)
|
c = sample.after_load.constraints[False, False, True, False, False]
|
||||||
internal.is_constraint_satisfied_old.reset_mock()
|
internal.are_constraints_satisfied.assert_called_once_with(c, tol=1.0)
|
||||||
internal.add_constraint.assert_called_once_with(c3, name="c3")
|
internal.are_constraints_satisfied.reset_mock()
|
||||||
internal.add_constraint.reset_mock()
|
internal.add_constraints.assert_called_once_with(c)
|
||||||
|
internal.add_constraints.reset_mock()
|
||||||
|
|
||||||
# LearningSolver calls after_iteration (second time)
|
# LearningSolver calls after_iteration (second time)
|
||||||
should_repeat = component.iteration_cb(solver, instance, None)
|
should_repeat = component.iteration_cb(solver, instance, None)
|
||||||
assert not should_repeat
|
assert not should_repeat
|
||||||
|
|
||||||
# The lazy constraint pool should be empty by now, so no calls should be made
|
# The lazy constraint pool should be empty by now, so no calls should be made
|
||||||
internal.is_constraint_satisfied_old.assert_not_called()
|
internal.are_constraints_satisfied.assert_not_called()
|
||||||
internal.add_constraint.assert_not_called()
|
internal.add_constraints.assert_not_called()
|
||||||
|
|
||||||
# LearningSolver calls after_solve_mip
|
# LearningSolver calls after_solve_mip
|
||||||
component.after_solve_mip(
|
component.after_solve_mip(
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ def test_redundancy() -> None:
|
|||||||
solver.relax_constraints(["c1"])
|
solver.relax_constraints(["c1"])
|
||||||
stats = solver.solve_lp()
|
stats = solver.solve_lp()
|
||||||
assert stats.lp_value == 2.0
|
assert stats.lp_value == 2.0
|
||||||
assert solver.is_constraint_satisfied(["c1"]) == [False]
|
|
||||||
|
|
||||||
solver.enforce_constraints(["c1"])
|
solver.enforce_constraints(["c1"])
|
||||||
stats = solver.solve_lp()
|
stats = solver.solve_lp()
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ def test_knapsack() -> None:
|
|||||||
|
|
||||||
features = FeaturesExtractor().extract(instance, solver)
|
features = FeaturesExtractor().extract(instance, solver)
|
||||||
assert features.variables is not None
|
assert features.variables is not None
|
||||||
assert features.constraints_old is not None
|
|
||||||
assert features.instance is not None
|
assert features.instance is not None
|
||||||
|
|
||||||
assert_equals(
|
assert_equals(
|
||||||
@@ -66,22 +65,29 @@ def test_knapsack() -> None:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert_equals(
|
assert_equals(
|
||||||
_round_constraints(features.constraints_old),
|
_round(features.constraints),
|
||||||
{
|
ConstraintFeatures(
|
||||||
"eq_capacity": Constraint(
|
basis_status=("N",),
|
||||||
basis_status="N",
|
categories=("eq_capacity",),
|
||||||
category="eq_capacity",
|
dual_values=(13.538462,),
|
||||||
dual_value=13.538462,
|
names=("eq_capacity",),
|
||||||
lazy=False,
|
lazy=(False,),
|
||||||
lhs={"x[0]": 23.0, "x[1]": 26.0, "x[2]": 20.0, "x[3]": 18.0, "z": -1.0},
|
lhs=(
|
||||||
rhs=0.0,
|
(
|
||||||
sa_rhs_down=-24.0,
|
("x[0]", 23.0),
|
||||||
sa_rhs_up=1.9999999999999987,
|
("x[1]", 26.0),
|
||||||
sense="=",
|
("x[2]", 20.0),
|
||||||
slack=0.0,
|
("x[3]", 18.0),
|
||||||
user_features=[0.0],
|
("z", -1.0),
|
||||||
)
|
),
|
||||||
},
|
),
|
||||||
|
rhs=(0.0,),
|
||||||
|
sa_rhs_down=(-24.0,),
|
||||||
|
sa_rhs_up=(2.0,),
|
||||||
|
senses=("=",),
|
||||||
|
slacks=(0.0,),
|
||||||
|
user_features=((0.0,),),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
assert_equals(
|
assert_equals(
|
||||||
features.instance,
|
features.instance,
|
||||||
|
|||||||
Reference in New Issue
Block a user