Remove tuples from ConstraintFeatures

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

@ -88,10 +88,10 @@ class StaticLazyConstraintsComponent(Component):
assert constraints is not None assert constraints is not None
assert constraints.lazy is not None assert constraints.lazy is not None
assert constraints.names is not None assert constraints.names is not None
selected = tuple( selected = [
(constraints.lazy[i] and constraints.names[i] not in self.enforced_cids) (constraints.lazy[i] and constraints.names[i] not in self.enforced_cids)
for i in range(len(constraints.lazy)) for i in range(len(constraints.lazy))
) ]
n_removed = sum(selected) n_removed = sum(selected)
n_kept = sum(constraints.lazy) - n_removed n_kept = sum(constraints.lazy) - n_removed
self.pool = constraints[selected] self.pool = constraints[selected]
@ -174,7 +174,7 @@ class StaticLazyConstraintsComponent(Component):
self.pool, self.pool,
tol=self.violation_tolerance, tol=self.violation_tolerance,
) )
is_violated = tuple(not i for i in is_satisfied) is_violated = [not i for i in is_satisfied]
violated_constraints = self.pool[is_violated] violated_constraints = self.pool[is_violated]
satisfied_constraints = self.pool[is_satisfied] satisfied_constraints = self.pool[is_satisfied]
self.pool = satisfied_constraints self.pool = satisfied_constraints

@ -78,18 +78,18 @@ class VariableFeatures:
@dataclass @dataclass
class ConstraintFeatures: class ConstraintFeatures:
basis_status: Optional[Tuple[str, ...]] = None basis_status: Optional[List[str]] = None
categories: Optional[Tuple[Optional[Hashable], ...]] = None categories: Optional[List[Optional[Hashable]]] = None
dual_values: Optional[Tuple[float, ...]] = None dual_values: Optional[List[float]] = None
names: Optional[Tuple[str, ...]] = None names: Optional[List[str]] = None
lazy: Optional[Tuple[bool, ...]] = None lazy: Optional[List[bool]] = None
lhs: Optional[Tuple[Tuple[Tuple[str, float], ...], ...]] = None lhs: Optional[List[List[Tuple[str, float]]]] = None
rhs: Optional[Tuple[float, ...]] = None rhs: Optional[List[float]] = None
sa_rhs_down: Optional[Tuple[float, ...]] = None sa_rhs_down: Optional[List[float]] = None
sa_rhs_up: Optional[Tuple[float, ...]] = None sa_rhs_up: Optional[List[float]] = None
senses: Optional[Tuple[str, ...]] = None senses: Optional[List[str]] = None
slacks: Optional[Tuple[float, ...]] = None slacks: Optional[List[float]] = None
user_features: Optional[Tuple[Optional[Tuple[float, ...]], ...]] = None user_features: Optional[List[Optional[List[float]]]] = None
def to_list(self, index: int) -> List[float]: def to_list(self, index: int) -> List[float]:
features: List[float] = [] features: List[float] = []
@ -107,7 +107,7 @@ class ConstraintFeatures:
_clip(features) _clip(features)
return features return features
def __getitem__(self, selected: Tuple[bool, ...]) -> "ConstraintFeatures": def __getitem__(self, selected: List[bool]) -> "ConstraintFeatures":
return ConstraintFeatures( return ConstraintFeatures(
basis_status=self._filter(self.basis_status, selected), basis_status=self._filter(self.basis_status, selected),
categories=self._filter(self.categories, selected), categories=self._filter(self.categories, selected),
@ -125,12 +125,12 @@ class ConstraintFeatures:
def _filter( def _filter(
self, self,
obj: Optional[Tuple], obj: Optional[List],
selected: Tuple[bool, ...], selected: List[bool],
) -> Optional[Tuple]: ) -> Optional[List]:
if obj is None: if obj is None:
return None return None
return tuple(obj[i] for (i, selected_i) in enumerate(selected) if selected_i) return [obj[i] for (i, selected_i) in enumerate(selected) if selected_i]
@dataclass @dataclass
@ -217,7 +217,7 @@ class FeaturesExtractor:
if user_features_i is None: if user_features_i is None:
user_features.append(None) user_features.append(None)
else: else:
user_features.append(user_features_i) user_features.append(list(user_features_i))
features.variables.categories = categories features.variables.categories = categories
features.variables.user_features = user_features features.variables.user_features = user_features
@ -229,7 +229,7 @@ class FeaturesExtractor:
assert features.constraints is not None assert features.constraints is not None
assert features.constraints.names 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()
user_features: List[Optional[Tuple[float, ...]]] = [] user_features: List[Optional[List[float]]] = []
categories: List[Optional[Hashable]] = [] categories: List[Optional[Hashable]] = []
lazy: List[bool] = [] lazy: List[bool] = []
for (cidx, cname) in enumerate(features.constraints.names): for (cidx, cname) in enumerate(features.constraints.names):
@ -253,7 +253,7 @@ class FeaturesExtractor:
f"Constraint features must be a list of numbers. " f"Constraint features must be a list of numbers. "
f"Found {type(f).__name__} instead for cname={cname}." f"Found {type(f).__name__} instead for cname={cname}."
) )
user_features.append(tuple(cf)) user_features.append(list(cf))
else: else:
user_features.append(None) user_features.append(None)
categories.append(None) categories.append(None)
@ -261,9 +261,9 @@ class FeaturesExtractor:
lazy.append(instance.is_constraint_lazy(cname)) lazy.append(instance.is_constraint_lazy(cname))
else: else:
lazy.append(False) lazy.append(False)
features.constraints.user_features = tuple(user_features) features.constraints.user_features = user_features
features.constraints.lazy = tuple(lazy) features.constraints.lazy = lazy
features.constraints.categories = tuple(categories) features.constraints.categories = categories
def _extract_user_features_instance( def _extract_user_features_instance(
self, self,

@ -36,3 +36,10 @@ class _RedirectOutput:
) -> None: ) -> None:
sys.stdout = self._original_stdout sys.stdout = self._original_stdout
sys.stderr = self._original_stderr sys.stderr = self._original_stderr
def _none_if_empty(obj: Any) -> Any:
if len(obj) == 0:
return None
else:
return obj

@ -122,7 +122,7 @@ class GurobiSolver(InternalSolver):
self, self,
cf: ConstraintFeatures, cf: ConstraintFeatures,
tol: float = 1e-5, tol: float = 1e-5,
) -> Tuple[bool, ...]: ) -> List[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
@ -141,7 +141,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 tuple(result) return result
@overrides @overrides
def build_test_instance_infeasible(self) -> Instance: def build_test_instance_infeasible(self) -> Instance:
@ -209,37 +209,37 @@ class GurobiSolver(InternalSolver):
raise Exception(f"unknown cbasis: {v}") raise Exception(f"unknown cbasis: {v}")
gp_constrs = model.getConstrs() gp_constrs = model.getConstrs()
constr_names = tuple(model.getAttr("constrName", gp_constrs)) constr_names = model.getAttr("constrName", gp_constrs)
rhs, lhs, senses, slacks, basis_status = None, None, None, None, None lhs: Optional[List] = None
rhs, senses, slacks, basis_status = None, None, None, None
dual_value, basis_status, sa_rhs_up, sa_rhs_down = None, None, None, None dual_value, basis_status, sa_rhs_up, sa_rhs_down = None, None, None, None
if with_static: if with_static:
rhs = tuple(model.getAttr("rhs", gp_constrs)) rhs = model.getAttr("rhs", gp_constrs)
senses = tuple(model.getAttr("sense", gp_constrs)) senses = model.getAttr("sense", gp_constrs)
if with_lhs: if with_lhs:
lhs_l: List = [None for _ in gp_constrs] lhs = [None for _ in gp_constrs]
for (i, gp_constr) in enumerate(gp_constrs): for (i, gp_constr) in enumerate(gp_constrs):
expr = model.getRow(gp_constr) expr = model.getRow(gp_constr)
lhs_l[i] = tuple( lhs[i] = [
(self._var_names[expr.getVar(j).index], expr.getCoeff(j)) (self._var_names[expr.getVar(j).index], expr.getCoeff(j))
for j in range(expr.size()) for j in range(expr.size())
) ]
lhs = tuple(lhs_l)
if self._has_lp_solution: if self._has_lp_solution:
dual_value = tuple(model.getAttr("pi", gp_constrs)) dual_value = model.getAttr("pi", gp_constrs)
basis_status = tuple( basis_status = list(
map( map(
_parse_gurobi_cbasis, _parse_gurobi_cbasis,
model.getAttr("cbasis", gp_constrs), model.getAttr("cbasis", gp_constrs),
) )
) )
if with_sa: if with_sa:
sa_rhs_up = tuple(model.getAttr("saRhsUp", gp_constrs)) sa_rhs_up = model.getAttr("saRhsUp", gp_constrs)
sa_rhs_down = tuple(model.getAttr("saRhsLow", gp_constrs)) sa_rhs_down = model.getAttr("saRhsLow", gp_constrs)
if self._has_lp_solution or self._has_mip_solution: if self._has_lp_solution or self._has_mip_solution:
slacks = tuple(model.getAttr("slack", gp_constrs)) slacks = model.getAttr("slack", gp_constrs)
return ConstraintFeatures( return ConstraintFeatures(
basis_status=basis_status, basis_status=basis_status,
@ -370,7 +370,7 @@ class GurobiSolver(InternalSolver):
return self.model.status in [self.gp.GRB.INFEASIBLE, self.gp.GRB.INF_OR_UNBD] return self.model.status in [self.gp.GRB.INFEASIBLE, self.gp.GRB.INF_OR_UNBD]
@overrides @overrides
def remove_constraints(self, names: Tuple[str, ...]) -> None: 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, List, Optional, Tuple from typing import Any, List, Optional, List
from miplearn.features import VariableFeatures, ConstraintFeatures from miplearn.features import VariableFeatures, ConstraintFeatures
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
@ -59,7 +59,7 @@ class InternalSolver(ABC):
self, self,
cf: ConstraintFeatures, cf: ConstraintFeatures,
tol: float = 1e-5, tol: float = 1e-5,
) -> Tuple[bool, ...]: ) -> List[bool]:
""" """
Checks whether the current solution satisfies the given constraints. Checks whether the current solution satisfies the given constraints.
""" """
@ -176,7 +176,7 @@ class InternalSolver(ABC):
pass pass
@abstractmethod @abstractmethod
def remove_constraints(self, names: Tuple[str, ...]) -> None: def remove_constraints(self, names: List[str]) -> None:
""" """
Removes the given constraints from the model. Removes the given constraints from the model.
""" """

@ -21,7 +21,7 @@ from pyomo.opt.base.solvers import SolverFactory
from miplearn.features import VariableFeatures, ConstraintFeatures from miplearn.features import VariableFeatures, ConstraintFeatures
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
from miplearn.solvers import _RedirectOutput from miplearn.solvers import _RedirectOutput, _none_if_empty
from miplearn.solvers.internal import ( from miplearn.solvers.internal import (
InternalSolver, InternalSolver,
LPSolveStats, LPSolveStats,
@ -113,7 +113,7 @@ class BasePyomoSolver(InternalSolver):
self, self,
cf: ConstraintFeatures, cf: ConstraintFeatures,
tol: float = 1e-5, tol: float = 1e-5,
) -> Tuple[bool, ...]: ) -> List[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
@ -130,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 tuple(result) return result
@overrides @overrides
def build_test_instance_infeasible(self) -> Instance: def build_test_instance_infeasible(self) -> Instance:
@ -165,7 +165,7 @@ class BasePyomoSolver(InternalSolver):
names: List[str] = [] names: List[str] = []
rhs: List[float] = [] rhs: List[float] = []
lhs: List[Tuple[Tuple[str, float], ...]] = [] lhs: List[List[Tuple[str, float]]] = []
senses: List[str] = [] senses: List[str] = []
dual_values: List[float] = [] dual_values: List[float] = []
slacks: List[float] = [] slacks: List[float] = []
@ -214,7 +214,7 @@ class BasePyomoSolver(InternalSolver):
raise Exception( raise Exception(
f"Unknown expression type: {expr.__class__.__name__}" f"Unknown expression type: {expr.__class__.__name__}"
) )
lhs.append(tuple(lhsc)) lhs.append(lhsc)
# Extract dual values # Extract dual values
if self._has_lp_solution: if self._has_lp_solution:
@ -233,24 +233,13 @@ class BasePyomoSolver(InternalSolver):
names.append(constr.name) names.append(constr.name)
_parse_constraint(constr) _parse_constraint(constr)
rhs_t, lhs_t, senses_t = None, None, None
slacks_t, dual_values_t = None, None
if with_static:
rhs_t = tuple(rhs)
lhs_t = tuple(lhs)
senses_t = tuple(senses)
if self._has_lp_solution:
dual_values_t = tuple(dual_values)
if self._has_lp_solution or self._has_mip_solution:
slacks_t = tuple(slacks)
return ConstraintFeatures( return ConstraintFeatures(
names=tuple(names), names=_none_if_empty(names),
rhs=rhs_t, rhs=_none_if_empty(rhs),
senses=senses_t, senses=_none_if_empty(senses),
lhs=lhs_t, lhs=_none_if_empty(lhs),
slacks=slacks_t, slacks=_none_if_empty(slacks),
dual_values=dual_values_t, dual_values=_none_if_empty(dual_values),
) )
@overrides @overrides
@ -337,12 +326,6 @@ class BasePyomoSolver(InternalSolver):
if self._has_lp_solution or self._has_mip_solution: if self._has_lp_solution or self._has_mip_solution:
values.append(v.value) values.append(v.value)
def _none_if_empty(obj: Any) -> Any:
if len(obj) == 0:
return None
else:
return obj
return VariableFeatures( return VariableFeatures(
names=_none_if_empty(names), names=_none_if_empty(names),
types=_none_if_empty(types), types=_none_if_empty(types),
@ -379,7 +362,7 @@ class BasePyomoSolver(InternalSolver):
return self._termination_condition == TerminationCondition.infeasible return self._termination_condition == TerminationCondition.infeasible
@overrides @overrides
def remove_constraints(self, names: Tuple[str, ...]) -> None: 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]

@ -69,18 +69,18 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
assert_equals( assert_equals(
solver.get_constraints(), solver.get_constraints(),
ConstraintFeatures( ConstraintFeatures(
names=("eq_capacity",), names=["eq_capacity"],
rhs=(0.0,), rhs=[0.0],
lhs=( lhs=[
( [
("x[0]", 23.0), ("x[0]", 23.0),
("x[1]", 26.0), ("x[1]", 26.0),
("x[2]", 20.0), ("x[2]", 20.0),
("x[3]", 18.0), ("x[3]", 18.0),
("z", -1.0), ("z", -1.0),
), ],
), ],
senses=("=",), senses=["="],
), ),
) )
@ -120,12 +120,12 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
_filter_attrs( _filter_attrs(
solver.get_constraint_attrs(), solver.get_constraint_attrs(),
ConstraintFeatures( ConstraintFeatures(
basis_status=("N",), basis_status=["N"],
dual_values=(13.538462,), dual_values=[13.538462],
names=("eq_capacity",), names=["eq_capacity"],
sa_rhs_down=(-24.0,), sa_rhs_down=[-24.0],
sa_rhs_up=(2.0,), sa_rhs_up=[2.0],
slacks=(0.0,), slacks=[0.0],
), ),
), ),
) )
@ -165,20 +165,20 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
_filter_attrs( _filter_attrs(
solver.get_constraint_attrs(), solver.get_constraint_attrs(),
ConstraintFeatures( ConstraintFeatures(
names=("eq_capacity",), names=["eq_capacity"],
slacks=(0.0,), slacks=[0.0],
), ),
), ),
) )
# Build new constraint and verify that it is violated # Build new constraint and verify that it is violated
cf = ConstraintFeatures( cf = ConstraintFeatures(
names=("cut",), names=["cut"],
lhs=((("x[0]", 1.0),),), lhs=[[("x[0]", 1.0)]],
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)
@ -187,28 +187,30 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
_filter_attrs( _filter_attrs(
solver.get_constraint_attrs(), solver.get_constraint_attrs(),
ConstraintFeatures( ConstraintFeatures(
names=("eq_capacity", "cut"), names=["eq_capacity", "cut"],
rhs=(0.0, 0.0), rhs=[0.0, 0.0],
lhs=( lhs=[
( [
("x[0]", 23.0), ("x[0]", 23.0),
("x[1]", 26.0), ("x[1]", 26.0),
("x[2]", 20.0), ("x[2]", 20.0),
("x[3]", 18.0), ("x[3]", 18.0),
("z", -1.0), ("z", -1.0),
), ],
(("x[0]", 1.0),), [
), ("x[0]", 1.0),
senses=("=", "<"), ],
],
senses=["=", "<"],
), ),
), ),
) )
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()

@ -33,20 +33,20 @@ def sample() -> Sample:
lazy_constraint_count=4, lazy_constraint_count=4,
), ),
constraints=ConstraintFeatures( constraints=ConstraintFeatures(
names=("c1", "c2", "c3", "c4", "c5"), names=["c1", "c2", "c3", "c4", "c5"],
categories=( categories=[
"type-a", "type-a",
"type-a", "type-a",
"type-a", "type-a",
"type-b", "type-b",
"type-b", "type-b",
), ],
lazy=(True, True, True, True, False), lazy=[True, True, True, True, False],
), ),
), ),
after_lp=Features( after_lp=Features(
instance=InstanceFeatures(), instance=InstanceFeatures(),
constraints=ConstraintFeatures(names=("c1", "c2", "c3", "c4", "c5")), constraints=ConstraintFeatures(names=["c1", "c2", "c3", "c4", "c5"]),
), ),
after_mip=Features( after_mip=Features(
extra={ extra={
@ -132,7 +132,7 @@ def test_usage_with_solver(instance: Instance) -> None:
# Should ask internal solver to remove some constraints # Should ask internal solver to remove some constraints
assert internal.remove_constraints.call_count == 1 assert internal.remove_constraints.call_count == 1
internal.remove_constraints.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)
@ -141,7 +141,7 @@ 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
assert sample.after_load.constraints is not None assert sample.after_load.constraints is not None
c = sample.after_load.constraints[False, False, True, False, False] c = sample.after_load.constraints[[False, False, True, False, False]]
internal.are_constraints_satisfied.assert_called_once_with(c, tol=1.0) internal.are_constraints_satisfied.assert_called_once_with(c, tol=1.0)
internal.are_constraints_satisfied.reset_mock() internal.are_constraints_satisfied.reset_mock()
internal.add_constraints.assert_called_once_with(c) internal.add_constraints.assert_called_once_with(c)

@ -65,26 +65,26 @@ def test_knapsack() -> None:
assert_equals( assert_equals(
_round(features.constraints), _round(features.constraints),
ConstraintFeatures( ConstraintFeatures(
basis_status=("N",), basis_status=["N"],
categories=("eq_capacity",), categories=["eq_capacity"],
dual_values=(13.538462,), dual_values=[13.538462],
names=("eq_capacity",), names=["eq_capacity"],
lazy=(False,), lazy=[False],
lhs=( lhs=[
( [
("x[0]", 23.0), ("x[0]", 23.0),
("x[1]", 26.0), ("x[1]", 26.0),
("x[2]", 20.0), ("x[2]", 20.0),
("x[3]", 18.0), ("x[3]", 18.0),
("z", -1.0), ("z", -1.0),
), ],
), ],
rhs=(0.0,), rhs=[0.0],
sa_rhs_down=(-24.0,), sa_rhs_down=[-24.0],
sa_rhs_up=(2.0,), sa_rhs_up=[2.0],
senses=("=",), senses=["="],
slacks=(0.0,), slacks=[0.0],
user_features=((0.0,),), user_features=[[0.0]],
), ),
) )
assert_equals( assert_equals(
@ -98,39 +98,39 @@ def test_knapsack() -> None:
def test_constraint_getindex() -> None: def test_constraint_getindex() -> None:
cf = ConstraintFeatures( cf = ConstraintFeatures(
names=("c1", "c2", "c3"), names=["c1", "c2", "c3"],
rhs=(1.0, 2.0, 3.0), rhs=[1.0, 2.0, 3.0],
senses=("=", "<", ">"), senses=["=", "<", ">"],
lhs=( lhs=[
( [
("x1", 1.0), ("x1", 1.0),
("x2", 1.0), ("x2", 1.0),
), ],
( [
("x2", 2.0), ("x2", 2.0),
("x3", 2.0), ("x3", 2.0),
), ],
( [
("x3", 3.0), ("x3", 3.0),
("x4", 3.0), ("x4", 3.0),
), ],
), ],
) )
assert_equals( assert_equals(
cf[True, False, True], cf[[True, False, True]],
ConstraintFeatures( ConstraintFeatures(
names=("c1", "c3"), names=["c1", "c3"],
rhs=(1.0, 3.0), rhs=[1.0, 3.0],
senses=("=", ">"), senses=["=", ">"],
lhs=( lhs=[
( [
("x1", 1.0), ("x1", 1.0),
("x2", 1.0), ("x2", 1.0),
), ],
( [
("x3", 3.0), ("x3", 3.0),
("x4", 3.0), ("x4", 3.0),
), ],
), ],
), ),
) )

Loading…
Cancel
Save