From f9ac65bf9c4e02753d83d3066fbdf143d839684f Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 20 May 2021 10:03:18 -0500 Subject: [PATCH] Remove tuples from VariableFeatures --- miplearn/features.py | 40 ++++++++++++------------- miplearn/solvers/gurobi.py | 38 ++++++++++++------------ miplearn/solvers/pyomo/base.py | 37 ++++++++--------------- miplearn/solvers/tests/__init__.py | 34 +++++++++++----------- tests/components/test_primal.py | 8 ++--- tests/problems/test_tsp.py | 6 ++-- tests/solvers/test_learning_solver.py | 4 +-- tests/test_features.py | 42 +++++++++++++-------------- 8 files changed, 98 insertions(+), 111 deletions(-) diff --git a/miplearn/features.py b/miplearn/features.py index 1665d9b..72746a7 100644 --- a/miplearn/features.py +++ b/miplearn/features.py @@ -30,22 +30,22 @@ class InstanceFeatures: @dataclass class VariableFeatures: - names: Optional[Tuple[str, ...]] = None - basis_status: Optional[Tuple[str, ...]] = None - categories: Optional[Tuple[Optional[Hashable], ...]] = None - lower_bounds: Optional[Tuple[float, ...]] = None - obj_coeffs: Optional[Tuple[float, ...]] = None - reduced_costs: Optional[Tuple[float, ...]] = None - sa_lb_down: Optional[Tuple[float, ...]] = None - sa_lb_up: Optional[Tuple[float, ...]] = None - sa_obj_down: Optional[Tuple[float, ...]] = None - sa_obj_up: Optional[Tuple[float, ...]] = None - sa_ub_down: Optional[Tuple[float, ...]] = None - sa_ub_up: Optional[Tuple[float, ...]] = None - types: Optional[Tuple[str, ...]] = None - upper_bounds: Optional[Tuple[float, ...]] = None - user_features: Optional[Tuple[Optional[Tuple[float, ...]], ...]] = None - values: Optional[Tuple[float, ...]] = None + names: Optional[List[str]] = None + basis_status: Optional[List[str]] = None + categories: Optional[List[Optional[Hashable]]] = None + lower_bounds: Optional[List[float]] = None + obj_coeffs: Optional[List[float]] = None + reduced_costs: Optional[List[float]] = None + sa_lb_down: Optional[List[float]] = None + sa_lb_up: Optional[List[float]] = None + sa_obj_down: Optional[List[float]] = None + sa_obj_up: Optional[List[float]] = None + sa_ub_down: Optional[List[float]] = None + sa_ub_up: Optional[List[float]] = None + types: Optional[List[str]] = None + upper_bounds: Optional[List[float]] = None + user_features: Optional[List[Optional[List[float]]]] = None + values: Optional[List[float]] = None # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based # approximation of strong branching. INFORMS Journal on Computing, 29(1), 185-195. @@ -190,7 +190,7 @@ class FeaturesExtractor: assert features.variables is not None assert features.variables.names is not None categories: List[Hashable] = [] - user_features: List[Optional[Tuple[float, ...]]] = [] + user_features: List[Optional[List[float]]] = [] for (i, var_name) in enumerate(features.variables.names): category: Hashable = instance.get_variable_category(var_name) user_features_i: Optional[List[float]] = None @@ -217,9 +217,9 @@ class FeaturesExtractor: if user_features_i is None: user_features.append(None) else: - user_features.append(tuple(user_features_i)) - features.variables.categories = tuple(categories) - features.variables.user_features = tuple(user_features) + user_features.append(user_features_i) + features.variables.categories = categories + features.variables.user_features = user_features def _extract_user_features_constrs( self, diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index e296e96..00a8dbd 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -75,12 +75,12 @@ class GurobiSolver(InternalSolver): self._cname_to_constr: Dict[str, "gurobipy.Constr"] = {} self._gp_vars: Tuple["gurobipy.Var", ...] = tuple() self._gp_constrs: Tuple["gurobipy.Constr", ...] = tuple() - self._var_names: Tuple[str, ...] = tuple() + self._var_names: List[str] = [] self._constr_names: Tuple[str, ...] = tuple() - self._var_types: Tuple[str, ...] = tuple() - self._var_lbs: Tuple[float, ...] = tuple() - self._var_ubs: Tuple[float, ...] = tuple() - self._var_obj_coeffs: Tuple[float, ...] = tuple() + self._var_types: List[str] = [] + self._var_lbs: List[float] = [] + self._var_ubs: List[float] = [] + self._var_obj_coeffs: List[float] = [] if self.lazy_cb_frequency == 1: self.lazy_cb_where = [self.gp.GRB.Callback.MIPSOL] @@ -328,8 +328,8 @@ class GurobiSolver(InternalSolver): obj_coeffs = self._var_obj_coeffs if self._has_lp_solution: - reduced_costs = tuple(model.getAttr("rc", self._gp_vars)) - basis_status = tuple( + reduced_costs = model.getAttr("rc", self._gp_vars) + basis_status = list( map( _parse_gurobi_vbasis, model.getAttr("vbasis", self._gp_vars), @@ -337,15 +337,15 @@ class GurobiSolver(InternalSolver): ) if with_sa: - sa_obj_up = tuple(model.getAttr("saobjUp", self._gp_vars)) - sa_obj_down = tuple(model.getAttr("saobjLow", self._gp_vars)) - sa_ub_up = tuple(model.getAttr("saubUp", self._gp_vars)) - sa_ub_down = tuple(model.getAttr("saubLow", self._gp_vars)) - sa_lb_up = tuple(model.getAttr("salbUp", self._gp_vars)) - sa_lb_down = tuple(model.getAttr("salbLow", self._gp_vars)) + sa_obj_up = model.getAttr("saobjUp", self._gp_vars) + sa_obj_down = model.getAttr("saobjLow", self._gp_vars) + sa_ub_up = model.getAttr("saubUp", self._gp_vars) + sa_ub_down = model.getAttr("saubLow", self._gp_vars) + sa_lb_up = model.getAttr("salbUp", self._gp_vars) + sa_lb_down = model.getAttr("salbLow", self._gp_vars) if model.solCount > 0: - values = tuple(model.getAttr("x", self._gp_vars)) + values = model.getAttr("x", self._gp_vars) return VariableFeatures( names=self._var_names, @@ -600,12 +600,12 @@ class GurobiSolver(InternalSolver): self._cname_to_constr = cname_to_constr self._gp_vars = tuple(gp_vars) self._gp_constrs = tuple(gp_constrs) - self._var_names = tuple(var_names) + self._var_names = var_names self._constr_names = tuple(constr_names) - self._var_types = tuple(var_types) - self._var_lbs = tuple(var_lbs) - self._var_ubs = tuple(var_ubs) - self._var_obj_coeffs = tuple(var_obj_coeffs) + self._var_types = var_types + self._var_lbs = var_lbs + self._var_ubs = var_ubs + self._var_obj_coeffs = var_obj_coeffs def __getstate__(self) -> Dict: return { diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index 3d46272..d26443a 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -337,33 +337,20 @@ class BasePyomoSolver(InternalSolver): if self._has_lp_solution or self._has_mip_solution: values.append(v.value) - types_t: Optional[Tuple[str, ...]] = None - upper_bounds_t: Optional[Tuple[float, ...]] = None - lower_bounds_t: Optional[Tuple[float, ...]] = None - obj_coeffs_t: Optional[Tuple[float, ...]] = None - reduced_costs_t: Optional[Tuple[float, ...]] = None - values_t: Optional[Tuple[float, ...]] = None - - if with_static: - types_t = tuple(types) - upper_bounds_t = tuple(upper_bounds) - lower_bounds_t = tuple(lower_bounds) - obj_coeffs_t = tuple(obj_coeffs) - - if self._has_lp_solution: - reduced_costs_t = tuple(reduced_costs) - - if self._has_lp_solution or self._has_mip_solution: - values_t = tuple(values) + def _none_if_empty(obj: Any) -> Any: + if len(obj) == 0: + return None + else: + return obj return VariableFeatures( - names=tuple(names), - types=types_t, - upper_bounds=upper_bounds_t, - lower_bounds=lower_bounds_t, - obj_coeffs=obj_coeffs_t, - reduced_costs=reduced_costs_t, - values=values_t, + names=_none_if_empty(names), + types=_none_if_empty(types), + upper_bounds=_none_if_empty(upper_bounds), + lower_bounds=_none_if_empty(lower_bounds), + obj_coeffs=_none_if_empty(obj_coeffs), + reduced_costs=_none_if_empty(reduced_costs), + values=_none_if_empty(values), ) @overrides diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index 5732786..fb18508 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -57,11 +57,11 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: assert_equals( solver.get_variables(), VariableFeatures( - names=("x[0]", "x[1]", "x[2]", "x[3]", "z"), - lower_bounds=(0.0, 0.0, 0.0, 0.0, 0.0), - upper_bounds=(1.0, 1.0, 1.0, 1.0, 67.0), - types=("B", "B", "B", "B", "C"), - obj_coeffs=(505.0, 352.0, 458.0, 220.0, 0.0), + names=["x[0]", "x[1]", "x[2]", "x[3]", "z"], + lower_bounds=[0.0, 0.0, 0.0, 0.0, 0.0], + upper_bounds=[1.0, 1.0, 1.0, 1.0, 67.0], + types=["B", "B", "B", "B", "C"], + obj_coeffs=[505.0, 352.0, 458.0, 220.0, 0.0], ), ) @@ -100,16 +100,16 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: _filter_attrs( solver.get_variable_attrs(), VariableFeatures( - names=("x[0]", "x[1]", "x[2]", "x[3]", "z"), - basis_status=("U", "B", "U", "L", "U"), - reduced_costs=(193.615385, 0.0, 187.230769, -23.692308, 13.538462), - sa_lb_down=(-inf, -inf, -inf, -0.111111, -inf), - sa_lb_up=(1.0, 0.923077, 1.0, 1.0, 67.0), - sa_obj_down=(311.384615, 317.777778, 270.769231, -inf, -13.538462), - sa_obj_up=(inf, 570.869565, inf, 243.692308, inf), - sa_ub_down=(0.913043, 0.923077, 0.9, 0.0, 43.0), - sa_ub_up=(2.043478, inf, 2.2, inf, 69.0), - values=(1.0, 0.923077, 1.0, 0.0, 67.0), + names=["x[0]", "x[1]", "x[2]", "x[3]", "z"], + basis_status=["U", "B", "U", "L", "U"], + reduced_costs=[193.615385, 0.0, 187.230769, -23.692308, 13.538462], + sa_lb_down=[-inf, -inf, -inf, -0.111111, -inf], + sa_lb_up=[1.0, 0.923077, 1.0, 1.0, 67.0], + sa_obj_down=[311.384615, 317.777778, 270.769231, -inf, -13.538462], + sa_obj_up=[inf, 570.869565, inf, 243.692308, inf], + sa_ub_down=[0.913043, 0.923077, 0.9, 0.0, 43.0], + sa_ub_up=[2.043478, inf, 2.2, inf, 69.0], + values=[1.0, 0.923077, 1.0, 0.0, 67.0], ), ), ) @@ -153,8 +153,8 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: _filter_attrs( solver.get_variable_attrs(), VariableFeatures( - names=("x[0]", "x[1]", "x[2]", "x[3]", "z"), - values=(1.0, 0.0, 1.0, 1.0, 61.0), + names=["x[0]", "x[1]", "x[2]", "x[3]", "z"], + values=[1.0, 0.0, 1.0, 1.0, 61.0], ), ), ) diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index 9614f9b..1f83bc7 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -29,8 +29,8 @@ def sample() -> Sample: after_load=Features( instance=InstanceFeatures(), variables=VariableFeatures( - names=("x[0]", "x[1]", "x[2]", "x[3]"), - categories=("default", None, "default", "default"), + names=["x[0]", "x[1]", "x[2]", "x[3]"], + categories=["default", None, "default", "default"], ), ), after_lp=Features( @@ -38,8 +38,8 @@ def sample() -> Sample: ), after_mip=Features( variables=VariableFeatures( - names=("x[0]", "x[1]", "x[2]", "x[3]"), - values=(0.0, 1.0, 1.0, 0.0), + names=["x[0]", "x[1]", "x[2]", "x[3]"], + values=[0.0, 1.0, 1.0, 0.0], ) ), ) diff --git a/tests/problems/test_tsp.py b/tests/problems/test_tsp.py index 82a7d76..4018dc4 100644 --- a/tests/problems/test_tsp.py +++ b/tests/problems/test_tsp.py @@ -44,7 +44,7 @@ def test_instance() -> None: features = instance.samples[0].after_mip assert features is not None assert features.variables is not None - assert features.variables.values == (1.0, 0.0, 1.0, 1.0, 0.0, 1.0) + assert features.variables.values == [1.0, 0.0, 1.0, 1.0, 0.0, 1.0] assert features.mip_solve is not None assert features.mip_solve.mip_lower_bound == 4.0 assert features.mip_solve.mip_upper_bound == 4.0 @@ -75,7 +75,7 @@ def test_subtour() -> None: assert lazy_enforced is not None assert len(lazy_enforced) > 0 assert features.variables is not None - assert features.variables.values == ( + assert features.variables.values == [ 1.0, 0.0, 0.0, @@ -91,6 +91,6 @@ def test_subtour() -> None: 0.0, 1.0, 1.0, - ) + ] solver.fit([instance]) solver.solve(instance) diff --git a/tests/solvers/test_learning_solver.py b/tests/solvers/test_learning_solver.py index 40fa9a1..f453b89 100644 --- a/tests/solvers/test_learning_solver.py +++ b/tests/solvers/test_learning_solver.py @@ -41,7 +41,7 @@ def test_learning_solver( after_mip = sample.after_mip assert after_mip is not None assert after_mip.variables is not None - assert after_mip.variables.values == (1.0, 0.0, 1.0, 1.0, 61.0) + assert after_mip.variables.values == [1.0, 0.0, 1.0, 1.0, 61.0] assert after_mip.mip_solve is not None assert after_mip.mip_solve.mip_lower_bound == 1183.0 assert after_mip.mip_solve.mip_upper_bound == 1183.0 @@ -51,7 +51,7 @@ def test_learning_solver( after_lp = sample.after_lp assert after_lp is not None assert after_lp.variables is not None - assert _round(after_lp.variables.values) == (1.0, 0.923077, 1.0, 0.0, 67.0) + assert _round(after_lp.variables.values) == [1.0, 0.923077, 1.0, 0.0, 67.0] assert after_lp.lp_solve is not None assert after_lp.lp_solve.lp_value is not None assert round(after_lp.lp_solve.lp_value, 3) == 1287.923 diff --git a/tests/test_features.py b/tests/test_features.py index bd9ded9..5d6739c 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -31,28 +31,28 @@ def test_knapsack() -> None: assert_equals( _round(features.variables), VariableFeatures( - names=("x[0]", "x[1]", "x[2]", "x[3]", "z"), - basis_status=("U", "B", "U", "L", "U"), - categories=("default", "default", "default", "default", None), - lower_bounds=(0.0, 0.0, 0.0, 0.0, 0.0), - obj_coeffs=(505.0, 352.0, 458.0, 220.0, 0.0), - reduced_costs=(193.615385, 0.0, 187.230769, -23.692308, 13.538462), - sa_lb_down=(-inf, -inf, -inf, -0.111111, -inf), - sa_lb_up=(1.0, 0.923077, 1.0, 1.0, 67.0), - sa_obj_down=(311.384615, 317.777778, 270.769231, -inf, -13.538462), - sa_obj_up=(inf, 570.869565, inf, 243.692308, inf), - sa_ub_down=(0.913043, 0.923077, 0.9, 0.0, 43.0), - sa_ub_up=(2.043478, inf, 2.2, inf, 69.0), - types=("B", "B", "B", "B", "C"), - upper_bounds=(1.0, 1.0, 1.0, 1.0, 67.0), - user_features=( - (23.0, 505.0), - (26.0, 352.0), - (20.0, 458.0), - (18.0, 220.0), + names=["x[0]", "x[1]", "x[2]", "x[3]", "z"], + basis_status=["U", "B", "U", "L", "U"], + categories=["default", "default", "default", "default", None], + lower_bounds=[0.0, 0.0, 0.0, 0.0, 0.0], + obj_coeffs=[505.0, 352.0, 458.0, 220.0, 0.0], + reduced_costs=[193.615385, 0.0, 187.230769, -23.692308, 13.538462], + sa_lb_down=[-inf, -inf, -inf, -0.111111, -inf], + sa_lb_up=[1.0, 0.923077, 1.0, 1.0, 67.0], + sa_obj_down=[311.384615, 317.777778, 270.769231, -inf, -13.538462], + sa_obj_up=[inf, 570.869565, inf, 243.692308, inf], + sa_ub_down=[0.913043, 0.923077, 0.9, 0.0, 43.0], + sa_ub_up=[2.043478, inf, 2.2, inf, 69.0], + types=["B", "B", "B", "B", "C"], + upper_bounds=[1.0, 1.0, 1.0, 1.0, 67.0], + user_features=[ + [23.0, 505.0], + [26.0, 352.0], + [20.0, 458.0], + [18.0, 220.0], None, - ), - values=(1.0, 0.923077, 1.0, 0.0, 67.0), + ], + values=[1.0, 0.923077, 1.0, 0.0, 67.0], alvarez_2017=[ [1.0, 0.32899, 0.0, 0.0, 1.0, 1.0, 5.265874, 46.051702], [1.0, 0.229316, 0.0, 0.076923, 1.0, 1.0, 3.532875, 5.388476],