From 061b1349fef7449650892cea9d1e854ea8b4e79f Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 1 Jul 2021 08:37:31 -0500 Subject: [PATCH 01/67] Move user_cuts/lazy_enforced to sample.data --- miplearn/components/dynamic_common.py | 27 +++++++------------- miplearn/components/dynamic_lazy.py | 4 +-- miplearn/components/dynamic_user_cuts.py | 4 +-- miplearn/components/static_lazy.py | 13 +++------- miplearn/features.py | 29 +++++++++++++++++----- miplearn/solvers/learning.py | 3 --- tests/components/test_dynamic_lazy.py | 6 ++--- tests/components/test_dynamic_user_cuts.py | 7 +++--- tests/components/test_static_lazy.py | 16 +++++------- tests/problems/test_tsp.py | 13 +++++----- 10 files changed, 56 insertions(+), 66 deletions(-) diff --git a/miplearn/components/dynamic_common.py b/miplearn/components/dynamic_common.py index c33e7c6..1eac460 100644 --- a/miplearn/components/dynamic_common.py +++ b/miplearn/components/dynamic_common.py @@ -81,13 +81,12 @@ class DynamicConstraintsComponent(Component): cids[category].append(cid) # Labels - if sample.after_mip is not None: - assert sample.after_mip.extra is not None - if sample.after_mip.extra[self.attr] is not None: - if cid in sample.after_mip.extra[self.attr]: - y[category] += [[False, True]] - else: - y[category] += [[True, False]] + enforced_cids = sample.get(self.attr) + if enforced_cids is not None: + if cid in enforced_cids: + y[category] += [[False, True]] + else: + y[category] += [[True, False]] return x, y, cids @overrides @@ -133,13 +132,7 @@ class DynamicConstraintsComponent(Component): @overrides def pre_sample_xy(self, instance: Instance, sample: Sample) -> Any: - if ( - sample.after_mip is None - or sample.after_mip.extra is None - or sample.after_mip.extra[self.attr] is None - ): - return - return sample.after_mip.extra[self.attr] + return sample.get(self.attr) @overrides def fit_xy( @@ -161,10 +154,8 @@ class DynamicConstraintsComponent(Component): instance: Instance, sample: Sample, ) -> Dict[Hashable, Dict[str, float]]: - assert sample.after_mip is not None - assert sample.after_mip.extra is not None - assert self.attr in sample.after_mip.extra - actual = sample.after_mip.extra[self.attr] + actual = sample.get(self.attr) + assert actual is not None pred = set(self.sample_predict(instance, sample)) tp: Dict[Hashable, int] = {} tn: Dict[Hashable, int] = {} diff --git a/miplearn/components/dynamic_lazy.py b/miplearn/components/dynamic_lazy.py index d0843d6..a30ec5e 100644 --- a/miplearn/components/dynamic_lazy.py +++ b/miplearn/components/dynamic_lazy.py @@ -78,9 +78,7 @@ class DynamicLazyConstraintsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - assert sample.after_mip is not None - assert sample.after_mip.extra is not None - sample.after_mip.extra["lazy_enforced"] = set(self.lazy_enforced) + sample.put("lazy_enforced", set(self.lazy_enforced)) @overrides def iteration_cb( diff --git a/miplearn/components/dynamic_user_cuts.py b/miplearn/components/dynamic_user_cuts.py index ec12dc0..fd8b141 100644 --- a/miplearn/components/dynamic_user_cuts.py +++ b/miplearn/components/dynamic_user_cuts.py @@ -87,9 +87,7 @@ class UserCutsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - assert sample.after_mip is not None - assert sample.after_mip.extra is not None - sample.after_mip.extra["user_cuts_enforced"] = set(self.enforced) + sample.put("user_cuts_enforced", set(self.enforced)) stats["UserCuts: Added in callback"] = self.n_added_in_callback if self.n_added_in_callback > 0: logger.info(f"{self.n_added_in_callback} user cuts added in callback") diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 141bbcb..46ffbb0 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -60,9 +60,7 @@ class StaticLazyConstraintsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - assert sample.after_mip is not None - assert sample.after_mip.extra is not None - sample.after_mip.extra["lazy_enforced"] = self.enforced_cids + sample.put("lazy_enforced", self.enforced_cids) stats["LazyStatic: Restored"] = self.n_restored stats["LazyStatic: Iterations"] = self.n_iterations @@ -236,12 +234,9 @@ class StaticLazyConstraintsComponent(Component): cids[category].append(cname) # Labels - if ( - (sample.after_mip is not None) - and (sample.after_mip.extra is not None) - and ("lazy_enforced" in sample.after_mip.extra) - ): - if cname in sample.after_mip.extra["lazy_enforced"]: + lazy_enforced = sample.get("lazy_enforced") + if lazy_enforced is not None: + if cname in lazy_enforced: y[category] += [[False, True]] else: y[category] += [[True, False]] diff --git a/miplearn/features.py b/miplearn/features.py index 1dccf03..fbad6d1 100644 --- a/miplearn/features.py +++ b/miplearn/features.py @@ -6,7 +6,7 @@ import collections import numbers from dataclasses import dataclass from math import log, isfinite -from typing import TYPE_CHECKING, Dict, Optional, List, Hashable, Tuple +from typing import TYPE_CHECKING, Dict, Optional, List, Hashable, Tuple, Any import numpy as np @@ -140,14 +140,31 @@ class Features: constraints: Optional[ConstraintFeatures] = None lp_solve: Optional["LPSolveStats"] = None mip_solve: Optional["MIPSolveStats"] = None - extra: Optional[Dict] = None -@dataclass class Sample: - after_load: Optional[Features] = None - after_lp: Optional[Features] = None - after_mip: Optional[Features] = None + def __init__( + self, + after_load: Optional[Features] = None, + after_lp: Optional[Features] = None, + after_mip: Optional[Features] = None, + data: Optional[Dict[str, Any]] = None, + ) -> None: + if data is None: + data = {} + self._data: Dict[str, Any] = data + self.after_load = after_load + self.after_lp = after_lp + self.after_mip = after_mip + + def get(self, key: str) -> Optional[Any]: + if key in self._data: + return self._data[key] + else: + return None + + def put(self, key: str, value: Any) -> None: + self._data[key] = value class FeaturesExtractor: diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index e7e07ee..2c8927a 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -173,7 +173,6 @@ class LearningSolver: "Features (after-load) extracted in %.2f seconds" % (time.time() - initial_time) ) - features.extra = {} sample.after_load = features callback_args = ( @@ -217,7 +216,6 @@ class LearningSolver: "Features (after-lp) extracted in %.2f seconds" % (time.time() - initial_time) ) - features.extra = {} features.lp_solve = lp_stats sample.after_lp = features @@ -291,7 +289,6 @@ class LearningSolver: % (time.time() - initial_time) ) features.mip_solve = mip_stats - features.extra = {} sample.after_mip = features # After-solve callbacks diff --git a/tests/components/test_dynamic_lazy.py b/tests/components/test_dynamic_lazy.py index bd43eaa..92b7e4d 100644 --- a/tests/components/test_dynamic_lazy.py +++ b/tests/components/test_dynamic_lazy.py @@ -28,11 +28,11 @@ def training_instances() -> List[Instance]: samples_0 = [ Sample( after_load=Features(instance=InstanceFeatures()), - after_mip=Features(extra={"lazy_enforced": {"c1", "c2"}}), + data={"lazy_enforced": {"c1", "c2"}}, ), Sample( after_load=Features(instance=InstanceFeatures()), - after_mip=Features(extra={"lazy_enforced": {"c2", "c3"}}), + data={"lazy_enforced": {"c2", "c3"}}, ), ] samples_0[0].after_load.instance.to_list = Mock(return_value=[5.0]) # type: ignore @@ -57,7 +57,7 @@ def training_instances() -> List[Instance]: samples_1 = [ Sample( after_load=Features(instance=InstanceFeatures()), - after_mip=Features(extra={"lazy_enforced": {"c3", "c4"}}), + data={"lazy_enforced": {"c3", "c4"}}, ) ] samples_1[0].after_load.instance.to_list = Mock(return_value=[8.0]) # type: ignore diff --git a/tests/components/test_dynamic_user_cuts.py b/tests/components/test_dynamic_user_cuts.py index c46a955..34bac63 100644 --- a/tests/components/test_dynamic_user_cuts.py +++ b/tests/components/test_dynamic_user_cuts.py @@ -81,10 +81,9 @@ def test_usage( ) -> None: stats_before = solver.solve(stab_instance) sample = stab_instance.get_samples()[0] - assert sample.after_mip is not None - assert sample.after_mip.extra is not None - assert len(sample.after_mip.extra["user_cuts_enforced"]) > 0 - print(stats_before) + user_cuts_enforced = sample.get("user_cuts_enforced") + assert user_cuts_enforced is not None + assert len(user_cuts_enforced) > 0 assert stats_before["UserCuts: Added ahead-of-time"] == 0 assert stats_before["UserCuts: Added in callback"] > 0 diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index 67c9c5d..7215af0 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -48,11 +48,9 @@ def sample() -> Sample: instance=InstanceFeatures(), constraints=ConstraintFeatures(names=["c1", "c2", "c3", "c4", "c5"]), ), - after_mip=Features( - extra={ - "lazy_enforced": {"c1", "c2", "c4"}, - } - ), + data={ + "lazy_enforced": {"c1", "c2", "c4"}, + }, ) sample.after_lp.instance.to_list = Mock(return_value=[5.0]) # type: ignore sample.after_lp.constraints.to_list = Mock( # type: ignore @@ -112,10 +110,7 @@ def test_usage_with_solver(instance: Instance) -> None: stats: LearningSolveStats = {} sample = instance.get_samples()[0] - assert sample.after_load is not None - assert sample.after_mip is not None - assert sample.after_mip.extra is not None - del sample.after_mip.extra["lazy_enforced"] + assert sample.get("lazy_enforced") is not None # LearningSolver calls before_solve_mip component.before_solve_mip( @@ -140,6 +135,7 @@ def test_usage_with_solver(instance: Instance) -> None: # Should ask internal solver to verify if constraints in the pool are # satisfied and add the ones that are not + assert sample.after_load is not None assert sample.after_load.constraints is not None c = sample.after_load.constraints[[False, False, True, False, False]] internal.are_constraints_satisfied.assert_called_once_with(c, tol=1.0) @@ -165,7 +161,7 @@ def test_usage_with_solver(instance: Instance) -> None: ) # Should update training sample - assert sample.after_mip.extra["lazy_enforced"] == {"c1", "c2", "c3", "c4"} + assert sample.get("lazy_enforced") == {"c1", "c2", "c3", "c4"} # # Should update stats assert stats["LazyStatic: Removed"] == 1 diff --git a/tests/problems/test_tsp.py b/tests/problems/test_tsp.py index 9b26ccd..cf4f98b 100644 --- a/tests/problems/test_tsp.py +++ b/tests/problems/test_tsp.py @@ -67,15 +67,14 @@ def test_subtour() -> None: instance = TravelingSalesmanInstance(n_cities, distances) solver = LearningSolver() solver.solve(instance) - assert len(instance.get_samples()) == 1 - sample = instance.get_samples()[0] - assert sample.after_mip is not None - features = sample.after_mip - assert features.extra is not None - assert "lazy_enforced" in features.extra - lazy_enforced = features.extra["lazy_enforced"] + samples = instance.get_samples() + assert len(samples) == 1 + sample = samples[0] + lazy_enforced = sample.get("lazy_enforced") assert lazy_enforced is not None assert len(lazy_enforced) > 0 + assert sample.after_mip is not None + features = sample.after_mip assert features.variables is not None assert features.variables.values == [ 1.0, From 7c4c3016114703a24bc439dac0e7c0f0631f4bbd Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 1 Jul 2021 11:06:36 -0500 Subject: [PATCH 02/67] Extract instance, var and constr features into sample --- miplearn/features.py | 326 ++++++++++++++++++++++++++++++++++- miplearn/solvers/learning.py | 3 + tests/test_features.py | 171 +++++++++++------- 3 files changed, 427 insertions(+), 73 deletions(-) diff --git a/miplearn/features.py b/miplearn/features.py index fbad6d1..b2dca96 100644 --- a/miplearn/features.py +++ b/miplearn/features.py @@ -4,6 +4,7 @@ import collections import numbers +from copy import copy from dataclasses import dataclass from math import log, isfinite from typing import TYPE_CHECKING, Dict, Optional, List, Hashable, Tuple, Any @@ -176,6 +177,95 @@ class FeaturesExtractor: self.with_sa = with_sa self.with_lhs = with_lhs + def extract_after_load_features( + self, + instance: "Instance", + solver: "InternalSolver", + sample: Sample, + ) -> None: + variables = solver.get_variables(with_static=True) + constraints = solver.get_constraints(with_static=True, with_lhs=self.with_lhs) + sample.put("var_lower_bounds", variables.lower_bounds) + sample.put("var_names", variables.names) + sample.put("var_obj_coeffs", variables.obj_coeffs) + sample.put("var_types", variables.types) + sample.put("var_upper_bounds", variables.upper_bounds) + sample.put("constr_names", constraints.names) + sample.put("constr_lhs", constraints.lhs) + sample.put("constr_rhs", constraints.rhs) + sample.put("constr_senses", constraints.senses) + self._extract_user_features_vars(instance, sample) + self._extract_user_features_constrs(instance, sample) + self._extract_user_features_instance(instance, sample) + self._extract_var_features_AlvLouWeh2017(sample) + sample.put( + "var_features", + self._combine( + sample, + [ + "var_features_AlvLouWeh2017", + "var_features_user", + "var_lower_bounds", + "var_obj_coeffs", + "var_upper_bounds", + ], + ), + ) + + def extract_after_lp_features( + self, + solver: "InternalSolver", + sample: Sample, + ) -> None: + variables = solver.get_variables(with_static=False, with_sa=self.with_sa) + constraints = solver.get_constraints(with_static=False, with_sa=self.with_sa) + sample.put("lp_var_basis_status", variables.basis_status) + sample.put("lp_var_reduced_costs", variables.reduced_costs) + sample.put("lp_var_sa_lb_down", variables.sa_lb_down) + sample.put("lp_var_sa_lb_up", variables.sa_lb_up) + sample.put("lp_var_sa_obj_down", variables.sa_obj_down) + sample.put("lp_var_sa_obj_up", variables.sa_obj_up) + sample.put("lp_var_sa_ub_down", variables.sa_ub_down) + sample.put("lp_var_sa_ub_up", variables.sa_ub_up) + sample.put("lp_var_values", variables.values) + sample.put("lp_constr_basis_status", constraints.basis_status) + sample.put("lp_constr_dual_values", constraints.dual_values) + sample.put("lp_constr_sa_rhs_down", constraints.sa_rhs_down) + sample.put("lp_constr_sa_rhs_up", constraints.sa_rhs_up) + sample.put("lp_constr_slacks", constraints.slacks) + self._extract_var_features_AlvLouWeh2017(sample, prefix="lp_") + sample.put( + "lp_var_features", + self._combine( + sample, + [ + "lp_var_features_AlvLouWeh2017", + "lp_var_reduced_costs", + "lp_var_sa_lb_down", + "lp_var_sa_lb_up", + "lp_var_sa_obj_down", + "lp_var_sa_obj_up", + "lp_var_sa_ub_down", + "lp_var_sa_ub_up", + "lp_var_values", + "var_features_user", + "var_lower_bounds", + "var_obj_coeffs", + "var_upper_bounds", + ], + ), + ) + + def extract_after_mip_features( + self, + solver: "InternalSolver", + sample: Sample, + ) -> None: + variables = solver.get_variables(with_static=False, with_sa=False) + constraints = solver.get_constraints(with_static=False, with_sa=False) + sample.put("mip_var_values", variables.values) + sample.put("mip_constr_slacks", constraints.slacks) + def extract( self, instance: "Instance", @@ -193,13 +283,107 @@ class FeaturesExtractor: with_lhs=self.with_lhs, ) if with_static: - self._extract_user_features_vars(instance, features) - self._extract_user_features_constrs(instance, features) - self._extract_user_features_instance(instance, features) - self._extract_alvarez_2017(features) + self._extract_user_features_vars_old(instance, features) + self._extract_user_features_constrs_old(instance, features) + self._extract_user_features_instance_old(instance, features) + self._extract_alvarez_2017_old(features) return features def _extract_user_features_vars( + self, + instance: "Instance", + sample: Sample, + ) -> None: + categories: List[Optional[Hashable]] = [] + user_features: List[Optional[List[float]]] = [] + var_features_dict = instance.get_variable_features() + var_categories_dict = instance.get_variable_categories() + var_names = sample.get("var_names") + assert var_names is not None + for (i, var_name) in enumerate(var_names): + if var_name not in var_categories_dict: + user_features.append(None) + categories.append(None) + continue + category: Hashable = var_categories_dict[var_name] + assert isinstance(category, collections.Hashable), ( + f"Variable category must be be hashable. " + f"Found {type(category).__name__} instead for var={var_name}." + ) + categories.append(category) + user_features_i: Optional[List[float]] = None + if var_name in var_features_dict: + user_features_i = var_features_dict[var_name] + if isinstance(user_features_i, np.ndarray): + user_features_i = user_features_i.tolist() + assert isinstance(user_features_i, list), ( + f"Variable features must be a list. " + f"Found {type(user_features_i).__name__} instead for " + f"var={var_name}." + ) + for v in user_features_i: + assert isinstance(v, numbers.Real), ( + f"Variable features must be a list of numbers. " + f"Found {type(v).__name__} instead " + f"for var={var_name}." + ) + user_features_i = list(user_features_i) + user_features.append(user_features_i) + sample.put("var_categories", categories) + sample.put("var_features_user", user_features) + + def _extract_user_features_constrs( + self, + instance: "Instance", + sample: Sample, + ) -> None: + has_static_lazy = instance.has_static_lazy_constraints() + user_features: List[Optional[List[float]]] = [] + categories: List[Optional[Hashable]] = [] + lazy: List[bool] = [] + constr_categories_dict = instance.get_constraint_categories() + constr_features_dict = instance.get_constraint_features() + constr_names = sample.get("constr_names") + assert constr_names is not None + + for (cidx, cname) in enumerate(constr_names): + category: Optional[Hashable] = cname + if cname in constr_categories_dict: + category = constr_categories_dict[cname] + if category is None: + user_features.append(None) + categories.append(None) + continue + assert isinstance(category, collections.Hashable), ( + f"Constraint category must be hashable. " + f"Found {type(category).__name__} instead for cname={cname}.", + ) + categories.append(category) + cf: Optional[List[float]] = None + if cname in constr_features_dict: + cf = constr_features_dict[cname] + if isinstance(cf, np.ndarray): + cf = cf.tolist() + assert isinstance(cf, list), ( + f"Constraint features must be a list. " + f"Found {type(cf).__name__} instead for cname={cname}." + ) + 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}." + ) + cf = list(cf) + user_features.append(cf) + if has_static_lazy: + lazy.append(instance.is_constraint_lazy(cname)) + else: + lazy.append(False) + sample.put("constr_features_user", user_features) + sample.put("constr_lazy", lazy) + sample.put("constr_categories", categories) + + def _extract_user_features_vars_old( self, instance: "Instance", features: Features, @@ -243,7 +427,7 @@ class FeaturesExtractor: features.variables.categories = categories features.variables.user_features = user_features - def _extract_user_features_constrs( + def _extract_user_features_constrs_old( self, instance: "Instance", features: Features, @@ -295,6 +479,28 @@ class FeaturesExtractor: features.constraints.categories = categories def _extract_user_features_instance( + self, + instance: "Instance", + sample: Sample, + ) -> None: + user_features = instance.get_instance_features() + if isinstance(user_features, np.ndarray): + user_features = user_features.tolist() + assert isinstance(user_features, list), ( + f"Instance features must be a list. " + f"Found {type(user_features).__name__} instead." + ) + for v in user_features: + assert isinstance(v, numbers.Real), ( + f"Instance features must be a list of numbers. " + f"Found {type(v).__name__} instead." + ) + constr_lazy = sample.get("constr_lazy") + assert constr_lazy is not None + sample.put("instance_features_user", user_features) + sample.put("static_lazy_count", sum(constr_lazy)) + + def _extract_user_features_instance_old( self, instance: "Instance", features: Features, @@ -318,7 +524,7 @@ class FeaturesExtractor: lazy_constraint_count=sum(features.constraints.lazy), ) - def _extract_alvarez_2017(self, features: Features) -> None: + def _extract_alvarez_2017_old(self, features: Features) -> None: assert features.variables is not None assert features.variables.names is not None @@ -394,6 +600,114 @@ class FeaturesExtractor: assert isfinite(v), f"non-finite elements detected: {f}" features.variables.alvarez_2017.append(f) + # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based + # approximation of strong branching. INFORMS Journal on Computing, 29(1), 185-195. + def _extract_var_features_AlvLouWeh2017( + self, + sample: Sample, + prefix: str = "", + ) -> None: + obj_coeffs = sample.get("var_obj_coeffs") + obj_sa_down = sample.get("lp_var_sa_obj_down") + obj_sa_up = sample.get("lp_var_sa_obj_up") + values = sample.get(f"lp_var_values") + assert obj_coeffs is not None + + pos_obj_coeff_sum = 0.0 + neg_obj_coeff_sum = 0.0 + for coeff in obj_coeffs: + if coeff > 0: + pos_obj_coeff_sum += coeff + if coeff < 0: + neg_obj_coeff_sum += -coeff + + features = [] + for i in range(len(obj_coeffs)): + f: List[float] = [] + if obj_coeffs is not None: + # Feature 1 + f.append(np.sign(obj_coeffs[i])) + + # Feature 2 + if pos_obj_coeff_sum > 0: + f.append(abs(obj_coeffs[i]) / pos_obj_coeff_sum) + else: + f.append(0.0) + + # Feature 3 + if neg_obj_coeff_sum > 0: + f.append(abs(obj_coeffs[i]) / neg_obj_coeff_sum) + else: + f.append(0.0) + + if values is not None: + # Feature 37 + f.append( + min( + values[i] - np.floor(values[i]), + np.ceil(values[i]) - values[i], + ) + ) + + if obj_sa_up is not None: + assert obj_sa_down is not None + assert obj_coeffs is not None + + # Convert inf into large finite numbers + sd = max(-1e20, obj_sa_down[i]) + su = min(1e20, obj_sa_up[i]) + obj = obj_coeffs[i] + + # Features 44 and 46 + f.append(np.sign(obj_sa_up[i])) + f.append(np.sign(obj_sa_down[i])) + + # Feature 47 + csign = np.sign(obj) + if csign != 0 and ((obj - sd) / csign) > 0.001: + f.append(log((obj - sd) / csign)) + else: + f.append(0.0) + + # Feature 48 + if csign != 0 and ((su - obj) / csign) > 0.001: + f.append(log((su - obj) / csign)) + else: + f.append(0.0) + + for v in f: + assert isfinite(v), f"non-finite elements detected: {f}" + features.append(f) + sample.put(f"{prefix}var_features_AlvLouWeh2017", features) + + def _combine( + self, + sample: Sample, + attrs: List[str], + ) -> List[List[float]]: + combined: List[List[float]] = [] + for attr in attrs: + series = sample.get(attr) + if series is None: + continue + if len(combined) == 0: + for i in range(len(series)): + combined.append([]) + for (i, s) in enumerate(series): + if s is None: + continue + elif isinstance(s, list): + combined[i].extend([_clipf(sj) for sj in s]) + else: + combined[i].append(_clipf(s)) + return combined + + +def _clipf(vi: float) -> float: + if not isfinite(vi): + return max(min(vi, 1e20), -1e20) + return vi + def _clip(v: List[float]) -> None: for (i, vi) in enumerate(v): diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index 2c8927a..9917566 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -168,6 +168,9 @@ class LearningSolver: # ------------------------------------------------------- logger.info("Extracting features (after-load)...") initial_time = time.time() + self.extractor.extract_after_load_features( + instance, self.internal_solver, sample + ) features = self.extractor.extract(instance, self.internal_solver) logger.info( "Features (after-load) extracted in %.2f seconds" diff --git a/tests/test_features.py b/tests/test_features.py index f4fb485..f324a5a 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -9,6 +9,7 @@ from miplearn.features import ( InstanceFeatures, VariableFeatures, ConstraintFeatures, + Sample, ) from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.tests import assert_equals @@ -21,71 +22,113 @@ def test_knapsack() -> None: instance = solver.build_test_instance_knapsack() model = instance.to_model() solver.set_instance(instance, model) - solver.solve_lp() - - features = FeaturesExtractor().extract(instance, solver) - assert features.variables is not None - assert features.instance is not None + extractor = FeaturesExtractor() + sample = Sample() + # after-load + # ------------------------------------------------------- + extractor.extract_after_load_features(instance, solver, sample) + assert_equals(sample.get("var_names"), ["x[0]", "x[1]", "x[2]", "x[3]", "z"]) + assert_equals(sample.get("var_lower_bounds"), [0.0, 0.0, 0.0, 0.0, 0.0]) + assert_equals(sample.get("var_obj_coeffs"), [505.0, 352.0, 458.0, 220.0, 0.0]) + assert_equals(sample.get("var_types"), ["B", "B", "B", "B", "C"]) + assert_equals(sample.get("var_upper_bounds"), [1.0, 1.0, 1.0, 1.0, 67.0]) assert_equals( - 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], - None, - ], - 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], - [1.0, 0.298371, 0.0, 0.0, 1.0, 1.0, 5.232342, 46.051702], - [1.0, 0.143322, 0.0, 0.0, 1.0, -1.0, 46.051702, 3.16515], - [0.0, 0.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0], - ], - ), + sample.get("var_categories"), + ["default", "default", "default", "default", None], ) assert_equals( - features.constraints, - ConstraintFeatures( - basis_status=["N"], - categories=["eq_capacity"], - dual_values=[13.538462], - names=["eq_capacity"], - lazy=[False], - lhs=[ - [ - ("x[0]", 23.0), - ("x[1]", 26.0), - ("x[2]", 20.0), - ("x[3]", 18.0), - ("z", -1.0), - ], + sample.get("var_features_user"), + [[23.0, 505.0], [26.0, 352.0], [20.0, 458.0], [18.0, 220.0], None], + ) + assert_equals( + sample.get("var_features_AlvLouWeh2017"), + [ + [1.0, 0.32899, 0.0], + [1.0, 0.229316, 0.0], + [1.0, 0.298371, 0.0], + [1.0, 0.143322, 0.0], + [0.0, 0.0, 0.0], + ], + ) + assert sample.get("var_features") is not None + assert_equals(sample.get("constr_names"), ["eq_capacity"]) + assert_equals( + sample.get("constr_lhs"), + [ + [ + ("x[0]", 23.0), + ("x[1]", 26.0), + ("x[2]", 20.0), + ("x[3]", 18.0), + ("z", -1.0), ], - rhs=[0.0], - sa_rhs_down=[-24.0], - sa_rhs_up=[2.0], - senses=["="], - slacks=[0.0], - user_features=[None], - ), + ], + ) + assert_equals(sample.get("constr_rhs"), [0.0]) + assert_equals(sample.get("constr_senses"), ["="]) + assert_equals(sample.get("constr_features_user"), [None]) + assert_equals(sample.get("constr_categories"), ["eq_capacity"]) + assert_equals(sample.get("constr_lazy"), [False]) + assert_equals(sample.get("instance_features_user"), [67.0, 21.75]) + assert_equals(sample.get("static_lazy_count"), 0) + + # after-lp + # ------------------------------------------------------- + solver.solve_lp() + extractor.extract_after_lp_features(solver, sample) + assert_equals( + sample.get("lp_var_basis_status"), + ["U", "B", "U", "L", "U"], + ) + assert_equals( + sample.get("lp_var_reduced_costs"), + [193.615385, 0.0, 187.230769, -23.692308, 13.538462], + ) + assert_equals( + sample.get("lp_var_sa_lb_down"), + [-inf, -inf, -inf, -0.111111, -inf], + ) + assert_equals( + sample.get("lp_var_sa_lb_up"), + [1.0, 0.923077, 1.0, 1.0, 67.0], + ) + assert_equals( + sample.get("lp_var_sa_obj_down"), + [311.384615, 317.777778, 270.769231, -inf, -13.538462], + ) + assert_equals( + sample.get("lp_var_sa_obj_up"), + [inf, 570.869565, inf, 243.692308, inf], ) + assert_equals(sample.get("lp_var_sa_ub_down"), [0.913043, 0.923077, 0.9, 0.0, 43.0]) + assert_equals(sample.get("lp_var_sa_ub_up"), [2.043478, inf, 2.2, inf, 69.0]) + assert_equals(sample.get("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0]) + assert_equals( + sample.get("lp_var_features_AlvLouWeh2017"), + [ + [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], + [1.0, 0.298371, 0.0, 0.0, 1.0, 1.0, 5.232342, 46.051702], + [1.0, 0.143322, 0.0, 0.0, 1.0, -1.0, 46.051702, 3.16515], + [0.0, 0.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0], + ], + ) + assert sample.get("lp_var_features") is not None + assert_equals(sample.get("lp_constr_basis_status"), ["N"]) + assert_equals(sample.get("lp_constr_dual_values"), [13.538462]) + assert_equals(sample.get("lp_constr_sa_rhs_down"), [-24.0]) + assert_equals(sample.get("lp_constr_sa_rhs_up"), [2.0]) + assert_equals(sample.get("lp_constr_slacks"), [0.0]) + + # after-mip + # ------------------------------------------------------- + solver.solve() + extractor.extract_after_mip_features(solver, sample) + assert_equals(sample.get("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 61.0]) + assert_equals(sample.get("mip_constr_slacks"), [0.0]) + + features = extractor.extract(instance, solver) assert_equals( features.instance, InstanceFeatures( @@ -138,10 +181,7 @@ def test_constraint_getindex() -> None: def test_assert_equals() -> None: assert_equals("hello", "hello") assert_equals([1.0, 2.0], [1.0, 2.0]) - assert_equals( - np.array([1.0, 2.0]), - np.array([1.0, 2.0]), - ) + assert_equals(np.array([1.0, 2.0]), np.array([1.0, 2.0])) assert_equals( np.array([[1.0, 2.0], [3.0, 4.0]]), np.array([[1.0, 2.0], [3.0, 4.0]]), @@ -150,9 +190,6 @@ def test_assert_equals() -> None: VariableFeatures(values=np.array([1.0, 2.0])), # type: ignore VariableFeatures(values=np.array([1.0, 2.0])), # type: ignore ) - assert_equals( - np.array([True, True]), - [True, True], - ) + assert_equals(np.array([True, True]), [True, True]) assert_equals((1.0,), (1.0,)) assert_equals({"x": 10}, {"x": 10}) From 4093ac62fd05e1c36a65b300750c2c54b60e0609 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 1 Jul 2021 11:45:19 -0500 Subject: [PATCH 03/67] Remove sample.after_mip --- miplearn/components/objective.py | 20 +++++++---------- miplearn/components/primal.py | 18 +++++++--------- miplearn/features.py | 2 -- miplearn/solvers/learning.py | 13 ++++++----- tests/components/test_objective.py | 10 ++++----- tests/components/test_primal.py | 10 ++++----- tests/problems/test_tsp.py | 16 ++++---------- tests/solvers/test_learning_solver.py | 31 +++++++++++---------------- 8 files changed, 46 insertions(+), 74 deletions(-) diff --git a/miplearn/components/objective.py b/miplearn/components/objective.py index 2fc5afd..33bc062 100644 --- a/miplearn/components/objective.py +++ b/miplearn/components/objective.py @@ -95,13 +95,12 @@ class ObjectiveValueComponent(Component): # Labels y: Dict[Hashable, List[List[float]]] = {} - if sample.after_mip is not None: - mip_stats = sample.after_mip.mip_solve - assert mip_stats is not None - if mip_stats.mip_lower_bound is not None: - y["Lower bound"] = [[mip_stats.mip_lower_bound]] - if mip_stats.mip_upper_bound is not None: - y["Upper bound"] = [[mip_stats.mip_upper_bound]] + mip_lower_bound = sample.get("mip_lower_bound") + mip_upper_bound = sample.get("mip_upper_bound") + if mip_lower_bound is not None: + y["Lower bound"] = [[mip_lower_bound]] + if mip_upper_bound is not None: + y["Upper bound"] = [[mip_upper_bound]] return x, y @@ -111,9 +110,6 @@ class ObjectiveValueComponent(Component): instance: Instance, sample: Sample, ) -> Dict[Hashable, Dict[str, float]]: - assert sample.after_mip is not None - assert sample.after_mip.mip_solve is not None - def compare(y_pred: float, y_actual: float) -> Dict[str, float]: err = np.round(abs(y_pred - y_actual), 8) return { @@ -125,8 +121,8 @@ class ObjectiveValueComponent(Component): result: Dict[Hashable, Dict[str, float]] = {} pred = self.sample_predict(sample) - actual_ub = sample.after_mip.mip_solve.mip_upper_bound - actual_lb = sample.after_mip.mip_solve.mip_lower_bound + actual_ub = sample.get("mip_upper_bound") + actual_lb = sample.get("mip_lower_bound") if actual_ub is not None: result["Upper bound"] = compare(pred["Upper bound"], actual_ub) if actual_lb is not None: diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index c37701d..9274745 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -155,6 +155,7 @@ class PrimalSolutionComponent(Component): assert sample.after_load.variables is not None assert sample.after_load.variables.names is not None assert sample.after_load.variables.categories is not None + mip_var_values = sample.get("mip_var_values") for (i, var_name) in enumerate(sample.after_load.variables.names): # Initialize categories @@ -174,10 +175,8 @@ class PrimalSolutionComponent(Component): x[category].append(features) # Labels - if sample.after_mip is not None: - assert sample.after_mip.variables is not None - assert sample.after_mip.variables.values is not None - opt_value = sample.after_mip.variables.values[i] + if mip_var_values is not None: + opt_value = mip_var_values[i] assert opt_value is not None assert 0.0 - 1e-5 <= opt_value <= 1.0 + 1e-5, ( f"Variable {var_name} has non-binary value {opt_value} in the " @@ -194,14 +193,13 @@ class PrimalSolutionComponent(Component): _: Optional[Instance], sample: Sample, ) -> Dict[Hashable, Dict[str, float]]: - assert sample.after_mip is not None - assert sample.after_mip.variables is not None - assert sample.after_mip.variables.values is not None - assert sample.after_mip.variables.names is not None + mip_var_values = sample.get("mip_var_values") + var_names = sample.get("var_names") + assert mip_var_values is not None + assert var_names is not None solution_actual = { - var_name: sample.after_mip.variables.values[i] - for (i, var_name) in enumerate(sample.after_mip.variables.names) + var_name: mip_var_values[i] for (i, var_name) in enumerate(var_names) } solution_pred = self.sample_predict(sample) vars_all, vars_one, vars_zero = set(), set(), set() diff --git a/miplearn/features.py b/miplearn/features.py index b2dca96..d913a00 100644 --- a/miplearn/features.py +++ b/miplearn/features.py @@ -148,7 +148,6 @@ class Sample: self, after_load: Optional[Features] = None, after_lp: Optional[Features] = None, - after_mip: Optional[Features] = None, data: Optional[Dict[str, Any]] = None, ) -> None: if data is None: @@ -156,7 +155,6 @@ class Sample: self._data: Dict[str, Any] = data self.after_load = after_load self.after_lp = after_lp - self.after_mip = after_mip def get(self, key: str) -> Optional[Any]: if key in self._data: diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index 9917566..cd5fb4c 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -210,6 +210,7 @@ class LearningSolver: # ------------------------------------------------------- logger.info("Extracting features (after-lp)...") initial_time = time.time() + self.extractor.extract_after_lp_features(self.internal_solver, sample) features = self.extractor.extract( instance, self.internal_solver, @@ -219,6 +220,8 @@ class LearningSolver: "Features (after-lp) extracted in %.2f seconds" % (time.time() - initial_time) ) + for (k, v) in lp_stats.__dict__.items(): + sample.put(k, v) features.lp_solve = lp_stats sample.after_lp = features @@ -282,17 +285,13 @@ class LearningSolver: # ------------------------------------------------------- logger.info("Extracting features (after-mip)...") initial_time = time.time() - features = self.extractor.extract( - instance, - self.internal_solver, - with_static=False, - ) + self.extractor.extract_after_mip_features(self.internal_solver, sample) + for (k, v) in mip_stats.__dict__.items(): + sample.put(k, v) logger.info( "Features (after-mip) extracted in %.2f seconds" % (time.time() - initial_time) ) - features.mip_solve = mip_stats - sample.after_mip = features # After-solve callbacks # ------------------------------------------------------- diff --git a/tests/components/test_objective.py b/tests/components/test_objective.py index bba86a7..6bb35ad 100644 --- a/tests/components/test_objective.py +++ b/tests/components/test_objective.py @@ -25,12 +25,10 @@ def sample() -> Sample: after_lp=Features( lp_solve=LPSolveStats(), ), - after_mip=Features( - mip_solve=MIPSolveStats( - mip_lower_bound=1.0, - mip_upper_bound=2.0, - ) - ), + data={ + "mip_lower_bound": 1.0, + "mip_upper_bound": 2.0, + }, ) sample.after_load.instance.to_list = Mock(return_value=[1.0, 2.0]) # type: ignore sample.after_lp.lp_solve.to_list = Mock(return_value=[3.0]) # type: ignore diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index 1f83bc7..acd4ef6 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -36,12 +36,10 @@ def sample() -> Sample: after_lp=Features( variables=VariableFeatures(), ), - after_mip=Features( - variables=VariableFeatures( - names=["x[0]", "x[1]", "x[2]", "x[3]"], - values=[0.0, 1.0, 1.0, 0.0], - ) - ), + data={ + "var_names": ["x[0]", "x[1]", "x[2]", "x[3]"], + "mip_var_values": [0.0, 1.0, 1.0, 0.0], + }, ) sample.after_load.instance.to_list = Mock(return_value=[5.0]) # type: ignore sample.after_load.variables.to_list = Mock( # type:ignore diff --git a/tests/problems/test_tsp.py b/tests/problems/test_tsp.py index cf4f98b..16b9628 100644 --- a/tests/problems/test_tsp.py +++ b/tests/problems/test_tsp.py @@ -41,14 +41,9 @@ def test_instance() -> None: solver.solve(instance) assert len(instance.get_samples()) == 1 sample = instance.get_samples()[0] - assert sample.after_mip is not None - features = sample.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.mip_solve is not None - assert features.mip_solve.mip_lower_bound == 4.0 - assert features.mip_solve.mip_upper_bound == 4.0 + assert sample.get("mip_var_values") == [1.0, 0.0, 1.0, 1.0, 0.0, 1.0] + assert sample.get("mip_lower_bound") == 4.0 + assert sample.get("mip_upper_bound") == 4.0 def test_subtour() -> None: @@ -73,10 +68,7 @@ def test_subtour() -> None: lazy_enforced = sample.get("lazy_enforced") assert lazy_enforced is not None assert len(lazy_enforced) > 0 - assert sample.after_mip is not None - features = sample.after_mip - assert features.variables is not None - assert features.variables.values == [ + assert sample.get("mip_var_values") == [ 1.0, 0.0, 0.0, diff --git a/tests/solvers/test_learning_solver.py b/tests/solvers/test_learning_solver.py index 19ad2b2..06b94db 100644 --- a/tests/solvers/test_learning_solver.py +++ b/tests/solvers/test_learning_solver.py @@ -38,25 +38,18 @@ def test_learning_solver( assert len(instance.get_samples()) > 0 sample = instance.get_samples()[0] - 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.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 - assert after_mip.mip_solve.mip_log is not None - assert len(after_mip.mip_solve.mip_log) > 100 - - after_lp = sample.after_lp - assert after_lp is not None - assert after_lp.variables is not None - assert_equals(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 - assert after_lp.lp_solve.lp_log is not None - assert len(after_lp.lp_solve.lp_log) > 100 + assert sample.get("mip_var_values") == [1.0, 0.0, 1.0, 1.0, 61.0] + assert sample.get("mip_lower_bound") == 1183.0 + assert sample.get("mip_upper_bound") == 1183.0 + mip_log = sample.get("mip_log") + assert mip_log is not None + assert len(mip_log) > 100 + + assert_equals(sample.get("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0]) + assert_equals(sample.get("lp_value"), 1287.923077) + lp_log = sample.get("lp_log") + assert lp_log is not None + assert len(lp_log) > 100 solver.fit([instance], n_jobs=4) solver.solve(instance) From b4a267a5240a1ad1c7eaab22a0c075b361da67d4 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 1 Jul 2021 12:25:50 -0500 Subject: [PATCH 04/67] Remove sample.after_lp --- miplearn/components/objective.py | 17 +++++-------- miplearn/components/primal.py | 15 +++++++---- miplearn/components/static_lazy.py | 37 +++++++++++++++------------- miplearn/features.py | 25 +++++++++++++++++-- miplearn/solvers/learning.py | 13 +++------- tests/components/test_objective.py | 9 +------ tests/components/test_primal.py | 25 ++++++++++--------- tests/components/test_static_lazy.py | 32 +++++++++++++----------- 8 files changed, 95 insertions(+), 78 deletions(-) diff --git a/miplearn/components/objective.py b/miplearn/components/objective.py index 33bc062..1c66ab8 100644 --- a/miplearn/components/objective.py +++ b/miplearn/components/objective.py @@ -77,20 +77,15 @@ class ObjectiveValueComponent(Component): _: Optional[Instance], sample: Sample, ) -> Tuple[Dict[Hashable, List[List[float]]], Dict[Hashable, List[List[float]]]]: - # Instance features - assert sample.after_load is not None - assert sample.after_load.instance is not None - f = sample.after_load.instance.to_list() - - # LP solve features - if sample.after_lp is not None: - assert sample.after_lp.lp_solve is not None - f.extend(sample.after_lp.lp_solve.to_list()) + lp_instance_features = sample.get("lp_instance_features") + if lp_instance_features is None: + lp_instance_features = sample.get("instance_features_user") + assert lp_instance_features is not None # Features x: Dict[Hashable, List[List[float]]] = { - "Upper bound": [f], - "Lower bound": [f], + "Upper bound": [lp_instance_features], + "Lower bound": [lp_instance_features], } # Labels diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index 9274745..fd39035 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -155,7 +155,15 @@ class PrimalSolutionComponent(Component): assert sample.after_load.variables is not None assert sample.after_load.variables.names is not None assert sample.after_load.variables.categories is not None + + instance_features = sample.get("instance_features_user") mip_var_values = sample.get("mip_var_values") + var_features = sample.get("lp_var_features") + if var_features is None: + var_features = sample.get("var_features") + + assert instance_features is not None + assert var_features is not None for (i, var_name) in enumerate(sample.after_load.variables.names): # Initialize categories @@ -167,11 +175,8 @@ class PrimalSolutionComponent(Component): y[category] = [] # Features - features = list(sample.after_load.instance.to_list()) - features.extend(sample.after_load.variables.to_list(i)) - if sample.after_lp is not None: - assert sample.after_lp.variables is not None - features.extend(sample.after_lp.variables.to_list(i)) + features = list(instance_features) + features.extend(var_features[i]) x[category].append(features) # Labels diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 46ffbb0..831c7a4 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -204,17 +204,26 @@ class StaticLazyConstraintsComponent(Component): x: Dict[Hashable, List[List[float]]] = {} y: Dict[Hashable, List[List[float]]] = {} cids: Dict[Hashable, List[str]] = {} - assert sample.after_load is not None - constraints = sample.after_load.constraints - 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): + instance_features = sample.get("instance_features_user") + constr_features = sample.get("lp_constr_features") + constr_names = sample.get("constr_names") + constr_categories = sample.get("constr_categories") + constr_lazy = sample.get("constr_lazy") + lazy_enforced = sample.get("lazy_enforced") + if constr_features is None: + constr_features = sample.get("constr_features_user") + + assert instance_features is not None + assert constr_features is not None + assert constr_names is not None + assert constr_categories is not None + assert constr_lazy is not None + + for (cidx, cname) in enumerate(constr_names): # Initialize categories - if not constraints.lazy[cidx]: + if not constr_lazy[cidx]: continue - category = constraints.categories[cidx] + category = constr_categories[cidx] if category is None: continue if category not in x: @@ -223,18 +232,12 @@ class StaticLazyConstraintsComponent(Component): cids[category] = [] # Features - sf = sample.after_load - if sample.after_lp is not None: - sf = sample.after_lp - assert sf.instance is not None - assert sf.constraints is not None - features = list(sf.instance.to_list()) - features.extend(sf.constraints.to_list(cidx)) + features = list(instance_features) + features.extend(constr_features[cidx]) x[category].append(features) cids[category].append(cname) # Labels - lazy_enforced = sample.get("lazy_enforced") if lazy_enforced is not None: if cname in lazy_enforced: y[category] += [[False, True]] diff --git a/miplearn/features.py b/miplearn/features.py index d913a00..d6943ed 100644 --- a/miplearn/features.py +++ b/miplearn/features.py @@ -147,14 +147,12 @@ class Sample: def __init__( self, after_load: Optional[Features] = None, - after_lp: Optional[Features] = None, data: Optional[Dict[str, Any]] = None, ) -> None: if data is None: data = {} self._data: Dict[str, Any] = data self.after_load = after_load - self.after_lp = after_lp def get(self, key: str) -> Optional[Any]: if key in self._data: @@ -253,6 +251,29 @@ class FeaturesExtractor: ], ), ) + sample.put( + "lp_constr_features", + self._combine( + sample, + [ + "constr_features_user", + "lp_constr_dual_values", + "lp_constr_sa_rhs_down", + "lp_constr_sa_rhs_up", + "lp_constr_slacks", + ], + ), + ) + instance_features_user = sample.get("instance_features_user") + assert instance_features_user is not None + sample.put( + "lp_instance_features", + instance_features_user + + [ + sample.get("lp_value"), + sample.get("lp_wallclock_time"), + ], + ) def extract_after_mip_features( self, diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index cd5fb4c..232625f 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -210,20 +210,13 @@ class LearningSolver: # ------------------------------------------------------- logger.info("Extracting features (after-lp)...") initial_time = time.time() + for (k, v) in lp_stats.__dict__.items(): + sample.put(k, v) self.extractor.extract_after_lp_features(self.internal_solver, sample) - features = self.extractor.extract( - instance, - self.internal_solver, - with_static=False, - ) logger.info( "Features (after-lp) extracted in %.2f seconds" % (time.time() - initial_time) ) - for (k, v) in lp_stats.__dict__.items(): - sample.put(k, v) - features.lp_solve = lp_stats - sample.after_lp = features # Callback wrappers # ------------------------------------------------------- @@ -285,9 +278,9 @@ class LearningSolver: # ------------------------------------------------------- logger.info("Extracting features (after-mip)...") initial_time = time.time() - self.extractor.extract_after_mip_features(self.internal_solver, sample) for (k, v) in mip_stats.__dict__.items(): sample.put(k, v) + self.extractor.extract_after_mip_features(self.internal_solver, sample) logger.info( "Features (after-mip) extracted in %.2f seconds" % (time.time() - initial_time) diff --git a/tests/components/test_objective.py b/tests/components/test_objective.py index 6bb35ad..324cb2c 100644 --- a/tests/components/test_objective.py +++ b/tests/components/test_objective.py @@ -19,19 +19,12 @@ from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver @pytest.fixture def sample() -> Sample: sample = Sample( - after_load=Features( - instance=InstanceFeatures(), - ), - after_lp=Features( - lp_solve=LPSolveStats(), - ), data={ "mip_lower_bound": 1.0, "mip_upper_bound": 2.0, + "lp_instance_features": [1.0, 2.0, 3.0], }, ) - sample.after_load.instance.to_list = Mock(return_value=[1.0, 2.0]) # type: ignore - sample.after_lp.lp_solve.to_list = Mock(return_value=[3.0]) # type: ignore return sample diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index acd4ef6..e262310 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -33,12 +33,23 @@ def sample() -> Sample: categories=["default", None, "default", "default"], ), ), - after_lp=Features( - variables=VariableFeatures(), - ), data={ "var_names": ["x[0]", "x[1]", "x[2]", "x[3]"], + "var_categories": ["default", None, "default", "default"], "mip_var_values": [0.0, 1.0, 1.0, 0.0], + "instance_features_user": [5.0], + "var_features": [ + [0.0, 0.0], + None, + [1.0, 0.0], + [1.0, 1.0], + ], + "lp_var_features": [ + [0.0, 0.0, 2.0, 2.0], + None, + [1.0, 0.0, 3.0, 2.0], + [1.0, 1.0, 3.0, 3.0], + ], }, ) sample.after_load.instance.to_list = Mock(return_value=[5.0]) # type: ignore @@ -50,14 +61,6 @@ def sample() -> Sample: [1.0, 1.0], ][i] ) - sample.after_lp.variables.to_list = Mock( # type:ignore - side_effect=lambda i: [ - [2.0, 2.0], - None, - [3.0, 2.0], - [3.0, 3.0], - ][i] - ) return sample diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index 7215af0..00284e9 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -44,24 +44,28 @@ def sample() -> Sample: lazy=[True, True, True, True, False], ), ), - after_lp=Features( - instance=InstanceFeatures(), - constraints=ConstraintFeatures(names=["c1", "c2", "c3", "c4", "c5"]), - ), data={ + "constr_categories": [ + "type-a", + "type-a", + "type-a", + "type-b", + "type-b", + ], + "constr_lazy": [True, True, True, True, False], + "constr_names": ["c1", "c2", "c3", "c4", "c5"], + "instance_features_user": [5.0], "lazy_enforced": {"c1", "c2", "c4"}, + "lp_constr_features": [ + [1.0, 1.0], + [1.0, 2.0], + [1.0, 3.0], + [1.0, 4.0, 0.0], + None, + ], + "static_lazy_count": 4, }, ) - sample.after_lp.instance.to_list = Mock(return_value=[5.0]) # type: ignore - sample.after_lp.constraints.to_list = Mock( # type: ignore - side_effect=lambda idx: { - 0: [1.0, 1.0], - 1: [1.0, 2.0], - 2: [1.0, 3.0], - 3: [1.0, 4.0, 0.0], - 4: None, - }[idx] - ) return sample From cd9e5d414485640a7abe93dd16ac72599b508172 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 6 Jul 2021 16:58:09 -0500 Subject: [PATCH 05/67] Remove sample.after_load --- miplearn/components/dynamic_common.py | 8 ++++---- miplearn/components/primal.py | 29 ++++++++++++--------------- miplearn/components/static_lazy.py | 9 ++++----- miplearn/features.py | 21 ++++++++++++++++--- miplearn/solvers/learning.py | 1 - tests/components/test_dynamic_lazy.py | 21 ++++++++++--------- tests/components/test_objective.py | 5 ++--- tests/components/test_primal.py | 18 +---------------- tests/components/test_static_lazy.py | 22 ++------------------ 9 files changed, 56 insertions(+), 78 deletions(-) diff --git a/miplearn/components/dynamic_common.py b/miplearn/components/dynamic_common.py index 1eac460..e6ca2d2 100644 --- a/miplearn/components/dynamic_common.py +++ b/miplearn/components/dynamic_common.py @@ -52,6 +52,8 @@ class DynamicConstraintsComponent(Component): cids: Dict[Hashable, List[str]] = {} constr_categories_dict = instance.get_constraint_categories() constr_features_dict = instance.get_constraint_features() + instance_features = sample.get("instance_features_user") + assert instance_features is not None for cid in self.known_cids: # Initialize categories if cid in constr_categories_dict: @@ -66,10 +68,8 @@ class DynamicConstraintsComponent(Component): cids[category] = [] # Features - features = [] - assert sample.after_load is not None - assert sample.after_load.instance is not None - features.extend(sample.after_load.instance.to_list()) + features: List[float] = [] + features.extend(instance_features) if cid in constr_features_dict: features.extend(constr_features_dict[cid]) for ci in features: diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index fd39035..2abc798 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -103,8 +103,10 @@ class PrimalSolutionComponent(Component): ) def sample_predict(self, sample: Sample) -> Solution: - assert sample.after_load is not None - assert sample.after_load.variables is not None + var_names = sample.get("var_names") + var_categories = sample.get("var_categories") + assert var_names is not None + assert var_categories is not None # Compute y_pred x, _ = self.sample_xy(None, sample) @@ -125,12 +127,10 @@ class PrimalSolutionComponent(Component): ).T # Convert y_pred into solution - assert sample.after_load.variables.names is not None - assert sample.after_load.variables.categories is not None - solution: Solution = {v: None for v in sample.after_load.variables.names} + solution: Solution = {v: None for v in var_names} category_offset: Dict[Hashable, int] = {cat: 0 for cat in x.keys()} - for (i, var_name) in enumerate(sample.after_load.variables.names): - category = sample.after_load.variables.categories[i] + for (i, var_name) in enumerate(var_names): + category = var_categories[i] if category not in category_offset: continue offset = category_offset[category] @@ -150,24 +150,21 @@ class PrimalSolutionComponent(Component): ) -> Tuple[Dict[Category, List[List[float]]], Dict[Category, List[List[float]]]]: x: Dict = {} y: Dict = {} - assert sample.after_load is not None - assert sample.after_load.instance is not None - assert sample.after_load.variables is not None - assert sample.after_load.variables.names is not None - assert sample.after_load.variables.categories is not None - instance_features = sample.get("instance_features_user") mip_var_values = sample.get("mip_var_values") var_features = sample.get("lp_var_features") + var_names = sample.get("var_names") + var_categories = sample.get("var_categories") if var_features is None: var_features = sample.get("var_features") - assert instance_features is not None assert var_features is not None + assert var_names is not None + assert var_categories is not None - for (i, var_name) in enumerate(sample.after_load.variables.names): + for (i, var_name) in enumerate(var_names): # Initialize categories - category = sample.after_load.variables.categories[i] + category = var_categories[i] if category is None: continue if category not in x.keys(): diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 831c7a4..5da2634 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -74,16 +74,15 @@ class StaticLazyConstraintsComponent(Component): sample: Sample, ) -> None: assert solver.internal_solver is not None - assert sample.after_load is not None - assert sample.after_load.instance is not None + static_lazy_count = sample.get("static_lazy_count") + assert static_lazy_count is not None logger.info("Predicting violated (static) lazy constraints...") - if sample.after_load.instance.lazy_constraint_count == 0: + if static_lazy_count == 0: logger.info("Instance does not have static lazy constraints. Skipping.") self.enforced_cids = set(self.sample_predict(sample)) logger.info("Moving lazy constraints to the pool...") - constraints = sample.after_load.constraints - assert constraints is not None + constraints = ConstraintFeatures.from_sample(sample) assert constraints.lazy is not None assert constraints.names is not None selected = [ diff --git a/miplearn/features.py b/miplearn/features.py index d6943ed..c12b426 100644 --- a/miplearn/features.py +++ b/miplearn/features.py @@ -82,9 +82,9 @@ class ConstraintFeatures: basis_status: Optional[List[str]] = None categories: Optional[List[Optional[Hashable]]] = None dual_values: Optional[List[float]] = None - names: Optional[List[str]] = None lazy: Optional[List[bool]] = None lhs: Optional[List[List[Tuple[str, float]]]] = None + names: Optional[List[str]] = None rhs: Optional[List[float]] = None sa_rhs_down: Optional[List[float]] = None sa_rhs_up: Optional[List[float]] = None @@ -92,6 +92,23 @@ class ConstraintFeatures: slacks: Optional[List[float]] = None user_features: Optional[List[Optional[List[float]]]] = None + @staticmethod + def from_sample(sample: "Sample") -> "ConstraintFeatures": + return ConstraintFeatures( + basis_status=sample.get("lp_constr_basis_status"), + categories=sample.get("constr_categories"), + dual_values=sample.get("lp_constr_dual_values"), + lazy=sample.get("constr_lazy"), + lhs=sample.get("constr_lhs"), + names=sample.get("constr_names"), + rhs=sample.get("constr_rhs"), + sa_rhs_down=sample.get("lp_constr_sa_rhs_down"), + sa_rhs_up=sample.get("lp_constr_sa_rhs_up"), + senses=sample.get("constr_senses"), + slacks=sample.get("lp_constr_slacks"), + user_features=sample.get("constr_features_user"), + ) + def to_list(self, index: int) -> List[float]: features: List[float] = [] for attr in [ @@ -146,13 +163,11 @@ class Features: class Sample: def __init__( self, - after_load: Optional[Features] = None, data: Optional[Dict[str, Any]] = None, ) -> None: if data is None: data = {} self._data: Dict[str, Any] = data - self.after_load = after_load def get(self, key: str) -> Optional[Any]: if key in self._data: diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index 232625f..240cf43 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -176,7 +176,6 @@ class LearningSolver: "Features (after-load) extracted in %.2f seconds" % (time.time() - initial_time) ) - sample.after_load = features callback_args = ( self, diff --git a/tests/components/test_dynamic_lazy.py b/tests/components/test_dynamic_lazy.py index 92b7e4d..27b182d 100644 --- a/tests/components/test_dynamic_lazy.py +++ b/tests/components/test_dynamic_lazy.py @@ -27,16 +27,18 @@ def training_instances() -> List[Instance]: instances = [cast(Instance, Mock(spec=Instance)) for _ in range(2)] samples_0 = [ Sample( - after_load=Features(instance=InstanceFeatures()), - data={"lazy_enforced": {"c1", "c2"}}, + { + "lazy_enforced": {"c1", "c2"}, + "instance_features_user": [5.0], + }, ), Sample( - after_load=Features(instance=InstanceFeatures()), - data={"lazy_enforced": {"c2", "c3"}}, + { + "lazy_enforced": {"c2", "c3"}, + "instance_features_user": [5.0], + }, ), ] - samples_0[0].after_load.instance.to_list = Mock(return_value=[5.0]) # type: ignore - samples_0[1].after_load.instance.to_list = Mock(return_value=[5.0]) # type: ignore instances[0].get_samples = Mock(return_value=samples_0) # type: ignore instances[0].get_constraint_categories = Mock( # type: ignore return_value={ @@ -56,11 +58,12 @@ def training_instances() -> List[Instance]: ) samples_1 = [ Sample( - after_load=Features(instance=InstanceFeatures()), - data={"lazy_enforced": {"c3", "c4"}}, + { + "lazy_enforced": {"c3", "c4"}, + "instance_features_user": [8.0], + }, ) ] - samples_1[0].after_load.instance.to_list = Mock(return_value=[8.0]) # type: ignore instances[1].get_samples = Mock(return_value=samples_1) # type: ignore instances[1].get_constraint_categories = Mock( # type: ignore return_value={ diff --git a/tests/components/test_objective.py b/tests/components/test_objective.py index 324cb2c..1a02dc7 100644 --- a/tests/components/test_objective.py +++ b/tests/components/test_objective.py @@ -10,8 +10,7 @@ from numpy.testing import assert_array_equal from miplearn.classifiers import Regressor from miplearn.components.objective import ObjectiveValueComponent -from miplearn.features import InstanceFeatures, Features, Sample -from miplearn.solvers.internal import MIPSolveStats, LPSolveStats +from miplearn.features import Sample from miplearn.solvers.learning import LearningSolver from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver @@ -19,7 +18,7 @@ from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver @pytest.fixture def sample() -> Sample: sample = Sample( - data={ + { "mip_lower_bound": 1.0, "mip_upper_bound": 2.0, "lp_instance_features": [1.0, 2.0, 3.0], diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index e262310..5925ee7 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -26,14 +26,7 @@ from miplearn.solvers.tests import assert_equals @pytest.fixture def sample() -> Sample: sample = Sample( - after_load=Features( - instance=InstanceFeatures(), - variables=VariableFeatures( - names=["x[0]", "x[1]", "x[2]", "x[3]"], - categories=["default", None, "default", "default"], - ), - ), - data={ + { "var_names": ["x[0]", "x[1]", "x[2]", "x[3]"], "var_categories": ["default", None, "default", "default"], "mip_var_values": [0.0, 1.0, 1.0, 0.0], @@ -52,15 +45,6 @@ def sample() -> Sample: ], }, ) - sample.after_load.instance.to_list = Mock(return_value=[5.0]) # type: ignore - sample.after_load.variables.to_list = Mock( # type:ignore - side_effect=lambda i: [ - [0.0, 0.0], - None, - [1.0, 0.0], - [1.0, 1.0], - ][i] - ) return sample diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index 00284e9..a43954f 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -28,23 +28,7 @@ from miplearn.types import ( @pytest.fixture def sample() -> Sample: sample = Sample( - after_load=Features( - instance=InstanceFeatures( - lazy_constraint_count=4, - ), - constraints=ConstraintFeatures( - names=["c1", "c2", "c3", "c4", "c5"], - categories=[ - "type-a", - "type-a", - "type-a", - "type-b", - "type-b", - ], - lazy=[True, True, True, True, False], - ), - ), - data={ + { "constr_categories": [ "type-a", "type-a", @@ -139,9 +123,7 @@ def test_usage_with_solver(instance: Instance) -> None: # Should ask internal solver to verify if constraints in the pool are # satisfied and add the ones that are not - assert sample.after_load is not None - assert sample.after_load.constraints is not None - c = sample.after_load.constraints[[False, False, True, False, False]] + c = ConstraintFeatures.from_sample(sample)[[False, False, True, False, False]] internal.are_constraints_satisfied.assert_called_once_with(c, tol=1.0) internal.are_constraints_satisfied.reset_mock() internal.add_constraints.assert_called_once_with(c) From c8c29138cae7189c3954be2c19b6456aa102e1ba Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 6 Jul 2021 17:04:32 -0500 Subject: [PATCH 06/67] Remove unused classes and functions --- miplearn/features.py | 295 +------------------------- miplearn/solvers/learning.py | 1 - tests/components/test_dynamic_lazy.py | 6 +- tests/components/test_primal.py | 7 +- tests/components/test_static_lazy.py | 7 +- tests/test_features.py | 10 - 6 files changed, 4 insertions(+), 322 deletions(-) diff --git a/miplearn/features.py b/miplearn/features.py index c12b426..1927d9c 100644 --- a/miplearn/features.py +++ b/miplearn/features.py @@ -4,7 +4,6 @@ import collections import numbers -from copy import copy from dataclasses import dataclass from math import log, isfinite from typing import TYPE_CHECKING, Dict, Optional, List, Hashable, Tuple, Any @@ -12,28 +11,14 @@ from typing import TYPE_CHECKING, Dict, Optional, List, Hashable, Tuple, Any import numpy as np if TYPE_CHECKING: - from miplearn.solvers.internal import InternalSolver, LPSolveStats, MIPSolveStats + from miplearn.solvers.internal import InternalSolver from miplearn.instance.base import Instance -@dataclass -class InstanceFeatures: - user_features: Optional[List[float]] = None - lazy_constraint_count: int = 0 - - def to_list(self) -> List[float]: - features: List[float] = [] - if self.user_features is not None: - features.extend(self.user_features) - _clip(features) - return features - - @dataclass class VariableFeatures: 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 @@ -45,42 +30,12 @@ class VariableFeatures: 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. - alvarez_2017: Optional[List[List[float]]] = None - - def to_list(self, index: int) -> List[float]: - features: List[float] = [] - for attr in [ - "lower_bounds", - "obj_coeffs", - "reduced_costs", - "sa_lb_down", - "sa_lb_up", - "sa_obj_down", - "sa_obj_up", - "sa_ub_down", - "sa_ub_up", - "upper_bounds", - "values", - ]: - if getattr(self, attr) is not None: - features.append(getattr(self, attr)[index]) - for attr in ["user_features", "alvarez_2017"]: - if getattr(self, attr) is not None: - if getattr(self, attr)[index] is not None: - features.extend(getattr(self, attr)[index]) - _clip(features) - return features - @dataclass class ConstraintFeatures: basis_status: Optional[List[str]] = None - categories: Optional[List[Optional[Hashable]]] = None dual_values: Optional[List[float]] = None lazy: Optional[List[bool]] = None lhs: Optional[List[List[Tuple[str, float]]]] = None @@ -90,13 +45,11 @@ class ConstraintFeatures: sa_rhs_up: Optional[List[float]] = None senses: Optional[List[str]] = None slacks: Optional[List[float]] = None - user_features: Optional[List[Optional[List[float]]]] = None @staticmethod def from_sample(sample: "Sample") -> "ConstraintFeatures": return ConstraintFeatures( basis_status=sample.get("lp_constr_basis_status"), - categories=sample.get("constr_categories"), dual_values=sample.get("lp_constr_dual_values"), lazy=sample.get("constr_lazy"), lhs=sample.get("constr_lhs"), @@ -106,29 +59,11 @@ class ConstraintFeatures: sa_rhs_up=sample.get("lp_constr_sa_rhs_up"), senses=sample.get("constr_senses"), slacks=sample.get("lp_constr_slacks"), - user_features=sample.get("constr_features_user"), ) - def to_list(self, index: int) -> List[float]: - features: List[float] = [] - for attr in [ - "dual_values", - "rhs", - "slacks", - ]: - if getattr(self, attr) is not None: - features.append(getattr(self, attr)[index]) - for attr in ["user_features"]: - if getattr(self, attr) is not None: - if getattr(self, attr)[index] is not None: - features.extend(getattr(self, attr)[index]) - _clip(features) - return features - def __getitem__(self, selected: List[bool]) -> "ConstraintFeatures": return ConstraintFeatures( basis_status=self._filter(self.basis_status, selected), - categories=self._filter(self.categories, selected), dual_values=self._filter(self.dual_values, selected), names=self._filter(self.names, selected), lazy=self._filter(self.lazy, selected), @@ -138,7 +73,6 @@ class ConstraintFeatures: sa_rhs_up=self._filter(self.sa_rhs_up, selected), senses=self._filter(self.senses, selected), slacks=self._filter(self.slacks, selected), - user_features=self._filter(self.user_features, selected), ) def _filter( @@ -151,15 +85,6 @@ class ConstraintFeatures: return [obj[i] for (i, selected_i) in enumerate(selected) if selected_i] -@dataclass -class Features: - instance: Optional[InstanceFeatures] = None - variables: Optional[VariableFeatures] = None - constraints: Optional[ConstraintFeatures] = None - lp_solve: Optional["LPSolveStats"] = None - mip_solve: Optional["MIPSolveStats"] = None - - class Sample: def __init__( self, @@ -300,29 +225,6 @@ class FeaturesExtractor: sample.put("mip_var_values", variables.values) sample.put("mip_constr_slacks", constraints.slacks) - def extract( - self, - instance: "Instance", - solver: "InternalSolver", - with_static: bool = True, - ) -> Features: - features = Features() - features.variables = solver.get_variables( - with_static=with_static, - with_sa=self.with_sa, - ) - features.constraints = solver.get_constraints( - with_static=with_static, - with_sa=self.with_sa, - with_lhs=self.with_lhs, - ) - if with_static: - self._extract_user_features_vars_old(instance, features) - self._extract_user_features_constrs_old(instance, features) - self._extract_user_features_instance_old(instance, features) - self._extract_alvarez_2017_old(features) - return features - def _extract_user_features_vars( self, instance: "Instance", @@ -417,101 +319,6 @@ class FeaturesExtractor: sample.put("constr_lazy", lazy) sample.put("constr_categories", categories) - def _extract_user_features_vars_old( - self, - instance: "Instance", - features: Features, - ) -> None: - assert features.variables is not None - assert features.variables.names is not None - categories: List[Optional[Hashable]] = [] - user_features: List[Optional[List[float]]] = [] - var_features_dict = instance.get_variable_features() - var_categories_dict = instance.get_variable_categories() - - for (i, var_name) in enumerate(features.variables.names): - if var_name not in var_categories_dict: - user_features.append(None) - categories.append(None) - continue - category: Hashable = var_categories_dict[var_name] - assert isinstance(category, collections.Hashable), ( - f"Variable category must be be hashable. " - f"Found {type(category).__name__} instead for var={var_name}." - ) - categories.append(category) - user_features_i: Optional[List[float]] = None - if var_name in var_features_dict: - user_features_i = var_features_dict[var_name] - if isinstance(user_features_i, np.ndarray): - user_features_i = user_features_i.tolist() - assert isinstance(user_features_i, list), ( - f"Variable features must be a list. " - f"Found {type(user_features_i).__name__} instead for " - f"var={var_name}." - ) - for v in user_features_i: - assert isinstance(v, numbers.Real), ( - f"Variable features must be a list of numbers. " - f"Found {type(v).__name__} instead " - f"for var={var_name}." - ) - user_features_i = list(user_features_i) - user_features.append(user_features_i) - features.variables.categories = categories - features.variables.user_features = user_features - - def _extract_user_features_constrs_old( - self, - instance: "Instance", - features: Features, - ) -> None: - assert features.constraints is not None - assert features.constraints.names is not None - has_static_lazy = instance.has_static_lazy_constraints() - user_features: List[Optional[List[float]]] = [] - categories: List[Optional[Hashable]] = [] - lazy: List[bool] = [] - constr_categories_dict = instance.get_constraint_categories() - constr_features_dict = instance.get_constraint_features() - - for (cidx, cname) in enumerate(features.constraints.names): - category: Optional[Hashable] = cname - if cname in constr_categories_dict: - category = constr_categories_dict[cname] - if category is None: - user_features.append(None) - categories.append(None) - continue - assert isinstance(category, collections.Hashable), ( - f"Constraint category must be hashable. " - f"Found {type(category).__name__} instead for cname={cname}.", - ) - categories.append(category) - cf: Optional[List[float]] = None - if cname in constr_features_dict: - cf = constr_features_dict[cname] - if isinstance(cf, np.ndarray): - cf = cf.tolist() - assert isinstance(cf, list), ( - f"Constraint features must be a list. " - f"Found {type(cf).__name__} instead for cname={cname}." - ) - 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}." - ) - cf = list(cf) - user_features.append(cf) - if has_static_lazy: - lazy.append(instance.is_constraint_lazy(cname)) - else: - lazy.append(False) - features.constraints.user_features = user_features - features.constraints.lazy = lazy - features.constraints.categories = categories - def _extract_user_features_instance( self, instance: "Instance", @@ -534,106 +341,6 @@ class FeaturesExtractor: sample.put("instance_features_user", user_features) sample.put("static_lazy_count", sum(constr_lazy)) - def _extract_user_features_instance_old( - self, - instance: "Instance", - features: Features, - ) -> None: - user_features = instance.get_instance_features() - if isinstance(user_features, np.ndarray): - user_features = user_features.tolist() - assert isinstance(user_features, list), ( - f"Instance features must be a list. " - f"Found {type(user_features).__name__} instead." - ) - for v in user_features: - assert isinstance(v, numbers.Real), ( - f"Instance features must be a list of numbers. " - f"Found {type(v).__name__} instead." - ) - assert features.constraints is not None - assert features.constraints.lazy is not None - features.instance = InstanceFeatures( - user_features=user_features, - lazy_constraint_count=sum(features.constraints.lazy), - ) - - def _extract_alvarez_2017_old(self, features: Features) -> None: - assert features.variables is not None - assert features.variables.names is not None - - obj_coeffs = features.variables.obj_coeffs - obj_sa_down = features.variables.sa_obj_down - obj_sa_up = features.variables.sa_obj_up - values = features.variables.values - - pos_obj_coeff_sum = 0.0 - neg_obj_coeff_sum = 0.0 - if obj_coeffs is not None: - for coeff in obj_coeffs: - if coeff > 0: - pos_obj_coeff_sum += coeff - if coeff < 0: - neg_obj_coeff_sum += -coeff - - features.variables.alvarez_2017 = [] - for i in range(len(features.variables.names)): - f: List[float] = [] - if obj_coeffs is not None: - # Feature 1 - f.append(np.sign(obj_coeffs[i])) - - # Feature 2 - if pos_obj_coeff_sum > 0: - f.append(abs(obj_coeffs[i]) / pos_obj_coeff_sum) - else: - f.append(0.0) - - # Feature 3 - if neg_obj_coeff_sum > 0: - f.append(abs(obj_coeffs[i]) / neg_obj_coeff_sum) - else: - f.append(0.0) - - if values is not None: - # Feature 37 - f.append( - min( - values[i] - np.floor(values[i]), - np.ceil(values[i]) - values[i], - ) - ) - - if obj_sa_up is not None: - assert obj_sa_down is not None - assert obj_coeffs is not None - - # Convert inf into large finite numbers - sd = max(-1e20, obj_sa_down[i]) - su = min(1e20, obj_sa_up[i]) - obj = obj_coeffs[i] - - # Features 44 and 46 - f.append(np.sign(obj_sa_up[i])) - f.append(np.sign(obj_sa_down[i])) - - # Feature 47 - csign = np.sign(obj) - if csign != 0 and ((obj - sd) / csign) > 0.001: - f.append(log((obj - sd) / csign)) - else: - f.append(0.0) - - # Feature 48 - if csign != 0 and ((su - obj) / csign) > 0.001: - f.append(log((su - obj) / csign)) - else: - f.append(0.0) - - for v in f: - assert isfinite(v), f"non-finite elements detected: {f}" - features.variables.alvarez_2017.append(f) - # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based # approximation of strong branching. INFORMS Journal on Computing, 29(1), 185-195. def _extract_var_features_AlvLouWeh2017( diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index 240cf43..04b8f89 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -171,7 +171,6 @@ class LearningSolver: self.extractor.extract_after_load_features( instance, self.internal_solver, sample ) - features = self.extractor.extract(instance, self.internal_solver) logger.info( "Features (after-load) extracted in %.2f seconds" % (time.time() - initial_time) diff --git a/tests/components/test_dynamic_lazy.py b/tests/components/test_dynamic_lazy.py index 27b182d..3963df6 100644 --- a/tests/components/test_dynamic_lazy.py +++ b/tests/components/test_dynamic_lazy.py @@ -11,11 +11,7 @@ from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import MinProbabilityThreshold from miplearn.components import classifier_evaluation_dict from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent -from miplearn.features import ( - Features, - InstanceFeatures, - Sample, -) +from miplearn.features import Sample from miplearn.instance.base import Instance from miplearn.solvers.tests import assert_equals diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index 5925ee7..e00ece3 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -12,12 +12,7 @@ from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import Threshold from miplearn.components import classifier_evaluation_dict from miplearn.components.primal import PrimalSolutionComponent -from miplearn.features import ( - Features, - Sample, - InstanceFeatures, - VariableFeatures, -) +from miplearn.features import Sample from miplearn.problems.tsp import TravelingSalesmanGenerator from miplearn.solvers.learning import LearningSolver from miplearn.solvers.tests import assert_equals diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index a43954f..c43e0bf 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -11,12 +11,7 @@ from numpy.testing import assert_array_equal from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import Threshold, MinProbabilityThreshold from miplearn.components.static_lazy import StaticLazyConstraintsComponent -from miplearn.features import ( - InstanceFeatures, - Features, - Sample, - ConstraintFeatures, -) +from miplearn.features import Sample, ConstraintFeatures from miplearn.instance.base import Instance from miplearn.solvers.internal import InternalSolver from miplearn.solvers.learning import LearningSolver diff --git a/tests/test_features.py b/tests/test_features.py index f324a5a..48947b4 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -6,7 +6,6 @@ import numpy as np from miplearn.features import ( FeaturesExtractor, - InstanceFeatures, VariableFeatures, ConstraintFeatures, Sample, @@ -128,15 +127,6 @@ def test_knapsack() -> None: assert_equals(sample.get("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 61.0]) assert_equals(sample.get("mip_constr_slacks"), [0.0]) - features = extractor.extract(instance, solver) - assert_equals( - features.instance, - InstanceFeatures( - user_features=[67.0, 21.75], - lazy_constraint_count=0, - ), - ) - def test_constraint_getindex() -> None: cf = ConstraintFeatures( From 609c5c7694ce6da34725f5351ffa679ef1c76cca Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 6 Jul 2021 17:08:22 -0500 Subject: [PATCH 07/67] Rename Variables and Constraints; move to internal.py --- miplearn/components/static_lazy.py | 7 ++- miplearn/features.py | 73 +----------------------- miplearn/solvers/gurobi.py | 15 ++--- miplearn/solvers/internal.py | 84 ++++++++++++++++++++++++++-- miplearn/solvers/pyomo/base.py | 15 ++--- miplearn/solvers/tests/__init__.py | 19 +++---- tests/components/test_static_lazy.py | 6 +- tests/test_features.py | 11 ++-- 8 files changed, 116 insertions(+), 114 deletions(-) diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 5da2634..c27f0ee 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -12,7 +12,8 @@ from miplearn.classifiers import Classifier from miplearn.classifiers.counting import CountingClassifier from miplearn.classifiers.threshold import MinProbabilityThreshold, Threshold from miplearn.components.component import Component -from miplearn.features import Sample, ConstraintFeatures +from miplearn.features import Sample +from miplearn.solvers.internal import Constraints from miplearn.instance.base import Instance from miplearn.types import LearningSolveStats @@ -45,7 +46,7 @@ class StaticLazyConstraintsComponent(Component): self.threshold_prototype: Threshold = threshold self.classifiers: Dict[Hashable, Classifier] = {} self.thresholds: Dict[Hashable, Threshold] = {} - self.pool: ConstraintFeatures = ConstraintFeatures() + self.pool: Constraints = Constraints() self.violation_tolerance: float = violation_tolerance self.enforced_cids: Set[Hashable] = set() self.n_restored: int = 0 @@ -82,7 +83,7 @@ class StaticLazyConstraintsComponent(Component): logger.info("Instance does not have static lazy constraints. Skipping.") self.enforced_cids = set(self.sample_predict(sample)) logger.info("Moving lazy constraints to the pool...") - constraints = ConstraintFeatures.from_sample(sample) + constraints = Constraints.from_sample(sample) assert constraints.lazy is not None assert constraints.names is not None selected = [ diff --git a/miplearn/features.py b/miplearn/features.py index 1927d9c..158c73c 100644 --- a/miplearn/features.py +++ b/miplearn/features.py @@ -4,9 +4,8 @@ import collections import numbers -from dataclasses import dataclass from math import log, isfinite -from typing import TYPE_CHECKING, Dict, Optional, List, Hashable, Tuple, Any +from typing import TYPE_CHECKING, Dict, Optional, List, Hashable, Any import numpy as np @@ -15,76 +14,6 @@ if TYPE_CHECKING: from miplearn.instance.base import Instance -@dataclass -class VariableFeatures: - names: Optional[List[str]] = None - basis_status: Optional[List[str]] = 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 - values: Optional[List[float]] = None - - -@dataclass -class ConstraintFeatures: - basis_status: Optional[List[str]] = None - dual_values: Optional[List[float]] = None - lazy: Optional[List[bool]] = None - lhs: Optional[List[List[Tuple[str, float]]]] = None - names: Optional[List[str]] = None - rhs: Optional[List[float]] = None - sa_rhs_down: Optional[List[float]] = None - sa_rhs_up: Optional[List[float]] = None - senses: Optional[List[str]] = None - slacks: Optional[List[float]] = None - - @staticmethod - def from_sample(sample: "Sample") -> "ConstraintFeatures": - return ConstraintFeatures( - basis_status=sample.get("lp_constr_basis_status"), - dual_values=sample.get("lp_constr_dual_values"), - lazy=sample.get("constr_lazy"), - lhs=sample.get("constr_lhs"), - names=sample.get("constr_names"), - rhs=sample.get("constr_rhs"), - sa_rhs_down=sample.get("lp_constr_sa_rhs_down"), - sa_rhs_up=sample.get("lp_constr_sa_rhs_up"), - senses=sample.get("constr_senses"), - slacks=sample.get("lp_constr_slacks"), - ) - - def __getitem__(self, selected: List[bool]) -> "ConstraintFeatures": - return ConstraintFeatures( - basis_status=self._filter(self.basis_status, selected), - dual_values=self._filter(self.dual_values, selected), - names=self._filter(self.names, selected), - lazy=self._filter(self.lazy, selected), - lhs=self._filter(self.lhs, selected), - rhs=self._filter(self.rhs, selected), - sa_rhs_down=self._filter(self.sa_rhs_down, selected), - sa_rhs_up=self._filter(self.sa_rhs_up, selected), - senses=self._filter(self.senses, selected), - slacks=self._filter(self.slacks, selected), - ) - - def _filter( - self, - obj: Optional[List], - selected: List[bool], - ) -> Optional[List]: - if obj is None: - return None - return [obj[i] for (i, selected_i) in enumerate(selected) if selected_i] - - class Sample: def __init__( self, diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index edfa182..52968cc 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -10,7 +10,6 @@ from typing import List, Any, Dict, Optional, Hashable, Tuple, TYPE_CHECKING from overrides import overrides -from miplearn.features import VariableFeatures, ConstraintFeatures from miplearn.instance.base import Instance from miplearn.solvers import _RedirectOutput from miplearn.solvers.internal import ( @@ -19,6 +18,8 @@ from miplearn.solvers.internal import ( IterationCallback, LazyCallback, MIPSolveStats, + Variables, + Constraints, ) from miplearn.solvers.pyomo.base import PyomoTestInstanceKnapsack from miplearn.types import ( @@ -91,7 +92,7 @@ class GurobiSolver(InternalSolver): ] @overrides - def add_constraints(self, cf: ConstraintFeatures) -> None: + def add_constraints(self, cf: Constraints) -> None: assert cf.names is not None assert cf.senses is not None assert cf.lhs is not None @@ -120,7 +121,7 @@ class GurobiSolver(InternalSolver): @overrides def are_constraints_satisfied( self, - cf: ConstraintFeatures, + cf: Constraints, tol: float = 1e-5, ) -> List[bool]: assert cf.names is not None @@ -196,7 +197,7 @@ class GurobiSolver(InternalSolver): with_static: bool = True, with_sa: bool = True, with_lhs: bool = True, - ) -> ConstraintFeatures: + ) -> Constraints: model = self.model assert model is not None assert model.numVars == len(self._gp_vars) @@ -241,7 +242,7 @@ class GurobiSolver(InternalSolver): if self._has_lp_solution or self._has_mip_solution: slacks = model.getAttr("slack", gp_constrs) - return ConstraintFeatures( + return Constraints( basis_status=basis_status, dual_values=dual_value, lhs=lhs, @@ -300,7 +301,7 @@ class GurobiSolver(InternalSolver): self, with_static: bool = True, with_sa: bool = True, - ) -> VariableFeatures: + ) -> Variables: model = self.model assert model is not None @@ -347,7 +348,7 @@ class GurobiSolver(InternalSolver): if model.solCount > 0: values = model.getAttr("x", self._gp_vars) - return VariableFeatures( + return Variables( names=self._var_names, upper_bounds=upper_bounds, lower_bounds=lower_bounds, diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index f689e44..1cdcfad 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -5,9 +5,8 @@ import logging from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, List, Optional, List +from typing import Any, Optional, List, Tuple, TYPE_CHECKING -from miplearn.features import VariableFeatures, ConstraintFeatures from miplearn.instance.base import Instance from miplearn.types import ( IterationCallback, @@ -18,6 +17,9 @@ from miplearn.types import ( logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from miplearn.features import Sample + @dataclass class LPSolveStats: @@ -44,20 +46,90 @@ class MIPSolveStats: mip_warm_start_value: Optional[float] = None +@dataclass +class Variables: + names: Optional[List[str]] = None + basis_status: Optional[List[str]] = 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 + values: Optional[List[float]] = None + + +@dataclass +class Constraints: + basis_status: Optional[List[str]] = None + dual_values: Optional[List[float]] = None + lazy: Optional[List[bool]] = None + lhs: Optional[List[List[Tuple[str, float]]]] = None + names: Optional[List[str]] = None + rhs: Optional[List[float]] = None + sa_rhs_down: Optional[List[float]] = None + sa_rhs_up: Optional[List[float]] = None + senses: Optional[List[str]] = None + slacks: Optional[List[float]] = None + + @staticmethod + def from_sample(sample: "Sample") -> "Constraints": + return Constraints( + basis_status=sample.get("lp_constr_basis_status"), + dual_values=sample.get("lp_constr_dual_values"), + lazy=sample.get("constr_lazy"), + lhs=sample.get("constr_lhs"), + names=sample.get("constr_names"), + rhs=sample.get("constr_rhs"), + sa_rhs_down=sample.get("lp_constr_sa_rhs_down"), + sa_rhs_up=sample.get("lp_constr_sa_rhs_up"), + senses=sample.get("constr_senses"), + slacks=sample.get("lp_constr_slacks"), + ) + + def __getitem__(self, selected: List[bool]) -> "Constraints": + return Constraints( + basis_status=self._filter(self.basis_status, selected), + dual_values=self._filter(self.dual_values, selected), + names=self._filter(self.names, selected), + lazy=self._filter(self.lazy, selected), + lhs=self._filter(self.lhs, selected), + rhs=self._filter(self.rhs, selected), + sa_rhs_down=self._filter(self.sa_rhs_down, selected), + sa_rhs_up=self._filter(self.sa_rhs_up, selected), + senses=self._filter(self.senses, selected), + slacks=self._filter(self.slacks, selected), + ) + + def _filter( + self, + obj: Optional[List], + selected: List[bool], + ) -> Optional[List]: + if obj is None: + return None + return [obj[i] for (i, selected_i) in enumerate(selected) if selected_i] + + class InternalSolver(ABC): """ Abstract class representing the MIP solver used internally by LearningSolver. """ @abstractmethod - def add_constraints(self, cf: ConstraintFeatures) -> None: + def add_constraints(self, cf: Constraints) -> None: """Adds the given constraints to the model.""" pass @abstractmethod def are_constraints_satisfied( self, - cf: ConstraintFeatures, + cf: Constraints, tol: float = 1e-5, ) -> List[bool]: """ @@ -133,7 +205,7 @@ class InternalSolver(ABC): with_static: bool = True, with_sa: bool = True, with_lhs: bool = True, - ) -> ConstraintFeatures: + ) -> Constraints: pass @abstractmethod @@ -149,7 +221,7 @@ class InternalSolver(ABC): self, with_static: bool = True, with_sa: bool = True, - ) -> VariableFeatures: + ) -> Variables: """ Returns a description of the decision variables in the problem. diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index 5889976..46b044c 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -19,7 +19,6 @@ from pyomo.core.expr.numeric_expr import SumExpression, MonomialTermExpression from pyomo.opt import TerminationCondition from pyomo.opt.base.solvers import SolverFactory -from miplearn.features import VariableFeatures, ConstraintFeatures from miplearn.instance.base import Instance from miplearn.solvers import _RedirectOutput, _none_if_empty from miplearn.solvers.internal import ( @@ -28,6 +27,8 @@ from miplearn.solvers.internal import ( IterationCallback, LazyCallback, MIPSolveStats, + Variables, + Constraints, ) from miplearn.types import ( SolverParams, @@ -79,7 +80,7 @@ class BasePyomoSolver(InternalSolver): self._has_mip_solution = False @overrides - def add_constraints(self, cf: ConstraintFeatures) -> None: + def add_constraints(self, cf: Constraints) -> None: assert cf.names is not None assert cf.senses is not None assert cf.lhs is not None @@ -111,7 +112,7 @@ class BasePyomoSolver(InternalSolver): @overrides def are_constraints_satisfied( self, - cf: ConstraintFeatures, + cf: Constraints, tol: float = 1e-5, ) -> List[bool]: assert cf.names is not None @@ -159,7 +160,7 @@ class BasePyomoSolver(InternalSolver): with_static: bool = True, with_sa: bool = True, with_lhs: bool = True, - ) -> ConstraintFeatures: + ) -> Constraints: model = self.model assert model is not None @@ -233,7 +234,7 @@ class BasePyomoSolver(InternalSolver): names.append(constr.name) _parse_constraint(constr) - return ConstraintFeatures( + return Constraints( names=_none_if_empty(names), rhs=_none_if_empty(rhs), senses=_none_if_empty(senses), @@ -271,7 +272,7 @@ class BasePyomoSolver(InternalSolver): self, with_static: bool = True, with_sa: bool = True, - ) -> VariableFeatures: + ) -> Variables: assert self.model is not None names: List[str] = [] @@ -326,7 +327,7 @@ class BasePyomoSolver(InternalSolver): if self._has_lp_solution or self._has_mip_solution: values.append(v.value) - return VariableFeatures( + return Variables( names=_none_if_empty(names), types=_none_if_empty(types), upper_bounds=_none_if_empty(upper_bounds), diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index 75c79b4..4626b4d 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -6,8 +6,7 @@ from typing import Any, List import numpy as np -from miplearn.features import VariableFeatures, ConstraintFeatures -from miplearn.solvers.internal import InternalSolver +from miplearn.solvers.internal import InternalSolver, Variables, Constraints inf = float("inf") @@ -40,7 +39,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: # Fetch variables (after-load) assert_equals( solver.get_variables(), - VariableFeatures( + Variables( 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], @@ -52,7 +51,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: # Fetch constraints (after-load) assert_equals( solver.get_constraints(), - ConstraintFeatures( + Constraints( names=["eq_capacity"], rhs=[0.0], lhs=[ @@ -83,7 +82,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_variables(with_static=False), _filter_attrs( solver.get_variable_attrs(), - VariableFeatures( + Variables( 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], @@ -103,7 +102,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_constraints(with_static=False), _filter_attrs( solver.get_constraint_attrs(), - ConstraintFeatures( + Constraints( basis_status=["N"], dual_values=[13.538462], names=["eq_capacity"], @@ -136,7 +135,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_variables(with_static=False), _filter_attrs( solver.get_variable_attrs(), - VariableFeatures( + Variables( names=["x[0]", "x[1]", "x[2]", "x[3]", "z"], values=[1.0, 0.0, 1.0, 1.0, 61.0], ), @@ -148,7 +147,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_constraints(with_static=False), _filter_attrs( solver.get_constraint_attrs(), - ConstraintFeatures( + Constraints( names=["eq_capacity"], slacks=[0.0], ), @@ -156,7 +155,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: ) # Build new constraint and verify that it is violated - cf = ConstraintFeatures( + cf = Constraints( names=["cut"], lhs=[[("x[0]", 1.0)]], rhs=[0.0], @@ -170,7 +169,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_constraints(with_static=True), _filter_attrs( solver.get_constraint_attrs(), - ConstraintFeatures( + Constraints( names=["eq_capacity", "cut"], rhs=[0.0, 0.0], lhs=[ diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index c43e0bf..727df8d 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -11,9 +11,9 @@ from numpy.testing import assert_array_equal from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import Threshold, MinProbabilityThreshold from miplearn.components.static_lazy import StaticLazyConstraintsComponent -from miplearn.features import Sample, ConstraintFeatures +from miplearn.features import Sample from miplearn.instance.base import Instance -from miplearn.solvers.internal import InternalSolver +from miplearn.solvers.internal import InternalSolver, Constraints from miplearn.solvers.learning import LearningSolver from miplearn.types import ( LearningSolveStats, @@ -118,7 +118,7 @@ def test_usage_with_solver(instance: Instance) -> None: # Should ask internal solver to verify if constraints in the pool are # satisfied and add the ones that are not - c = ConstraintFeatures.from_sample(sample)[[False, False, True, False, False]] + c = Constraints.from_sample(sample)[[False, False, True, False, False]] internal.are_constraints_satisfied.assert_called_once_with(c, tol=1.0) internal.are_constraints_satisfied.reset_mock() internal.add_constraints.assert_called_once_with(c) diff --git a/tests/test_features.py b/tests/test_features.py index 48947b4..2aa563c 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -6,10 +6,9 @@ import numpy as np from miplearn.features import ( FeaturesExtractor, - VariableFeatures, - ConstraintFeatures, Sample, ) +from miplearn.solvers.internal import Variables, Constraints from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.tests import assert_equals @@ -129,7 +128,7 @@ def test_knapsack() -> None: def test_constraint_getindex() -> None: - cf = ConstraintFeatures( + cf = Constraints( names=["c1", "c2", "c3"], rhs=[1.0, 2.0, 3.0], senses=["=", "<", ">"], @@ -150,7 +149,7 @@ def test_constraint_getindex() -> None: ) assert_equals( cf[[True, False, True]], - ConstraintFeatures( + Constraints( names=["c1", "c3"], rhs=[1.0, 3.0], senses=["=", ">"], @@ -177,8 +176,8 @@ def test_assert_equals() -> None: np.array([[1.0, 2.0], [3.0, 4.0]]), ) assert_equals( - VariableFeatures(values=np.array([1.0, 2.0])), # type: ignore - VariableFeatures(values=np.array([1.0, 2.0])), # type: ignore + Variables(values=np.array([1.0, 2.0])), # type: ignore + Variables(values=np.array([1.0, 2.0])), # type: ignore ) assert_equals(np.array([True, True]), [True, True]) assert_equals((1.0,), (1.0,)) From ed77d548aa0447ad5816bfe0a87100908f96fee4 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 14 Jul 2021 08:16:49 -0500 Subject: [PATCH 08/67] Remove unused function --- miplearn/features.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/miplearn/features.py b/miplearn/features.py index 158c73c..eec166b 100644 --- a/miplearn/features.py +++ b/miplearn/features.py @@ -367,19 +367,13 @@ class FeaturesExtractor: if s is None: continue elif isinstance(s, list): - combined[i].extend([_clipf(sj) for sj in s]) + combined[i].extend([_clip(sj) for sj in s]) else: - combined[i].append(_clipf(s)) + combined[i].append(_clip(s)) return combined -def _clipf(vi: float) -> float: +def _clip(vi: float) -> float: if not isfinite(vi): return max(min(vi, 1e20), -1e20) return vi - - -def _clip(v: List[float]) -> None: - for (i, vi) in enumerate(v): - if not isfinite(vi): - v[i] = max(min(vi, 1e20), -1e20) From 851b8001bb94c60bd3c710666e0af83febf1122d Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 14 Jul 2021 08:23:52 -0500 Subject: [PATCH 09/67] Move features to its own package --- miplearn/components/component.py | 2 +- miplearn/components/dynamic_common.py | 2 +- miplearn/components/dynamic_lazy.py | 2 +- miplearn/components/dynamic_user_cuts.py | 2 +- miplearn/components/objective.py | 2 +- miplearn/components/primal.py | 2 +- miplearn/components/static_lazy.py | 2 +- miplearn/features/__init__.py | 3 +++ .../{features.py => features/extractor.py} | 21 ++-------------- miplearn/features/sample.py | 24 +++++++++++++++++++ miplearn/instance/base.py | 2 +- miplearn/instance/picklegz.py | 2 +- miplearn/solvers/internal.py | 2 +- miplearn/solvers/learning.py | 3 ++- tests/components/test_dynamic_lazy.py | 2 +- tests/components/test_objective.py | 2 +- tests/components/test_primal.py | 2 +- tests/components/test_static_lazy.py | 2 +- tests/test_features.py | 6 ++--- 19 files changed, 47 insertions(+), 38 deletions(-) create mode 100644 miplearn/features/__init__.py rename miplearn/{features.py => features/extractor.py} (96%) create mode 100644 miplearn/features/sample.py diff --git a/miplearn/components/component.py b/miplearn/components/component.py index cf7e104..22a1cda 100644 --- a/miplearn/components/component.py +++ b/miplearn/components/component.py @@ -7,7 +7,7 @@ from typing import Any, List, TYPE_CHECKING, Tuple, Dict, Hashable, Optional import numpy as np from p_tqdm import p_umap -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.instance.base import Instance from miplearn.types import LearningSolveStats diff --git a/miplearn/components/dynamic_common.py b/miplearn/components/dynamic_common.py index e6ca2d2..3f69156 100644 --- a/miplearn/components/dynamic_common.py +++ b/miplearn/components/dynamic_common.py @@ -12,7 +12,7 @@ from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import Threshold from miplearn.components import classifier_evaluation_dict from miplearn.components.component import Component -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.instance.base import Instance logger = logging.getLogger(__name__) diff --git a/miplearn/components/dynamic_lazy.py b/miplearn/components/dynamic_lazy.py index a30ec5e..fdf56aa 100644 --- a/miplearn/components/dynamic_lazy.py +++ b/miplearn/components/dynamic_lazy.py @@ -13,7 +13,7 @@ from miplearn.classifiers.counting import CountingClassifier from miplearn.classifiers.threshold import MinProbabilityThreshold, Threshold from miplearn.components.component import Component from miplearn.components.dynamic_common import DynamicConstraintsComponent -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.instance.base import Instance from miplearn.types import LearningSolveStats diff --git a/miplearn/components/dynamic_user_cuts.py b/miplearn/components/dynamic_user_cuts.py index fd8b141..4db64e0 100644 --- a/miplearn/components/dynamic_user_cuts.py +++ b/miplearn/components/dynamic_user_cuts.py @@ -13,7 +13,7 @@ from miplearn.classifiers.counting import CountingClassifier from miplearn.classifiers.threshold import Threshold, MinProbabilityThreshold from miplearn.components.component import Component from miplearn.components.dynamic_common import DynamicConstraintsComponent -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.instance.base import Instance from miplearn.types import LearningSolveStats diff --git a/miplearn/components/objective.py b/miplearn/components/objective.py index 1c66ab8..de2852c 100644 --- a/miplearn/components/objective.py +++ b/miplearn/components/objective.py @@ -12,7 +12,7 @@ from sklearn.linear_model import LinearRegression from miplearn.classifiers import Regressor from miplearn.classifiers.sklearn import ScikitLearnRegressor from miplearn.components.component import Component -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.instance.base import Instance from miplearn.types import LearningSolveStats diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index 2abc798..b9d141f 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -21,7 +21,7 @@ from miplearn.classifiers.adaptive import AdaptiveClassifier from miplearn.classifiers.threshold import MinPrecisionThreshold, Threshold from miplearn.components import classifier_evaluation_dict from miplearn.components.component import Component -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.instance.base import Instance from miplearn.types import ( LearningSolveStats, diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index c27f0ee..2cad2a3 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -12,7 +12,7 @@ from miplearn.classifiers import Classifier from miplearn.classifiers.counting import CountingClassifier from miplearn.classifiers.threshold import MinProbabilityThreshold, Threshold from miplearn.components.component import Component -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.solvers.internal import Constraints from miplearn.instance.base import Instance from miplearn.types import LearningSolveStats diff --git a/miplearn/features/__init__.py b/miplearn/features/__init__.py new file mode 100644 index 0000000..5fbccb1 --- /dev/null +++ b/miplearn/features/__init__.py @@ -0,0 +1,3 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. diff --git a/miplearn/features.py b/miplearn/features/extractor.py similarity index 96% rename from miplearn/features.py rename to miplearn/features/extractor.py index eec166b..b26f97f 100644 --- a/miplearn/features.py +++ b/miplearn/features/extractor.py @@ -9,30 +9,13 @@ from typing import TYPE_CHECKING, Dict, Optional, List, Hashable, Any import numpy as np +from miplearn.features.sample import Sample + if TYPE_CHECKING: from miplearn.solvers.internal import InternalSolver from miplearn.instance.base import Instance -class Sample: - def __init__( - self, - data: Optional[Dict[str, Any]] = None, - ) -> None: - if data is None: - data = {} - self._data: Dict[str, Any] = data - - def get(self, key: str) -> Optional[Any]: - if key in self._data: - return self._data[key] - else: - return None - - def put(self, key: str, value: Any) -> None: - self._data[key] = value - - class FeaturesExtractor: def __init__( self, diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py new file mode 100644 index 0000000..ae85c72 --- /dev/null +++ b/miplearn/features/sample.py @@ -0,0 +1,24 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +from typing import Dict, Optional, Any + + +class Sample: + def __init__( + self, + data: Optional[Dict[str, Any]] = None, + ) -> None: + if data is None: + data = {} + self._data: Dict[str, Any] = data + + def get(self, key: str) -> Optional[Any]: + if key in self._data: + return self._data[key] + else: + return None + + def put(self, key: str, value: Any) -> None: + self._data[key] = value diff --git a/miplearn/instance/base.py b/miplearn/instance/base.py index c14df41..17817cf 100644 --- a/miplearn/instance/base.py +++ b/miplearn/instance/base.py @@ -6,7 +6,7 @@ import logging from abc import ABC, abstractmethod from typing import Any, List, Hashable, TYPE_CHECKING, Dict -from miplearn.features import Sample +from miplearn.features.sample import Sample logger = logging.getLogger(__name__) diff --git a/miplearn/instance/picklegz.py b/miplearn/instance/picklegz.py index 9cb4e2e..80ebcc6 100644 --- a/miplearn/instance/picklegz.py +++ b/miplearn/instance/picklegz.py @@ -10,7 +10,7 @@ from typing import Optional, Any, List, Hashable, cast, IO, TYPE_CHECKING, Dict from overrides import overrides -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.instance.base import Instance if TYPE_CHECKING: diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index 1cdcfad..f2e20d2 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -18,7 +18,7 @@ from miplearn.types import ( logger = logging.getLogger(__name__) if TYPE_CHECKING: - from miplearn.features import Sample + from miplearn.features.sample import Sample @dataclass diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index 04b8f89..986afdb 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -14,7 +14,8 @@ from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent from miplearn.components.dynamic_user_cuts import UserCutsComponent from miplearn.components.objective import ObjectiveValueComponent from miplearn.components.primal import PrimalSolutionComponent -from miplearn.features import FeaturesExtractor, Sample +from miplearn.features.extractor import FeaturesExtractor +from miplearn.features.sample import Sample from miplearn.instance.base import Instance from miplearn.instance.picklegz import PickleGzInstance from miplearn.solvers import _RedirectOutput diff --git a/tests/components/test_dynamic_lazy.py b/tests/components/test_dynamic_lazy.py index 3963df6..f29dc47 100644 --- a/tests/components/test_dynamic_lazy.py +++ b/tests/components/test_dynamic_lazy.py @@ -11,7 +11,7 @@ from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import MinProbabilityThreshold from miplearn.components import classifier_evaluation_dict from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.instance.base import Instance from miplearn.solvers.tests import assert_equals diff --git a/tests/components/test_objective.py b/tests/components/test_objective.py index 1a02dc7..84f5bc6 100644 --- a/tests/components/test_objective.py +++ b/tests/components/test_objective.py @@ -10,7 +10,7 @@ from numpy.testing import assert_array_equal from miplearn.classifiers import Regressor from miplearn.components.objective import ObjectiveValueComponent -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.solvers.learning import LearningSolver from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index e00ece3..16b6c2d 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -12,7 +12,7 @@ from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import Threshold from miplearn.components import classifier_evaluation_dict from miplearn.components.primal import PrimalSolutionComponent -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.problems.tsp import TravelingSalesmanGenerator from miplearn.solvers.learning import LearningSolver from miplearn.solvers.tests import assert_equals diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index 727df8d..ed4f91d 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -11,7 +11,7 @@ from numpy.testing import assert_array_equal from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import Threshold, MinProbabilityThreshold from miplearn.components.static_lazy import StaticLazyConstraintsComponent -from miplearn.features import Sample +from miplearn.features.sample import Sample from miplearn.instance.base import Instance from miplearn.solvers.internal import InternalSolver, Constraints from miplearn.solvers.learning import LearningSolver diff --git a/tests/test_features.py b/tests/test_features.py index 2aa563c..c7b5557 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -4,10 +4,8 @@ import numpy as np -from miplearn.features import ( - FeaturesExtractor, - Sample, -) +from miplearn.features.extractor import FeaturesExtractor +from miplearn.features.sample import Sample from miplearn.solvers.internal import Variables, Constraints from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.tests import assert_equals From 235c3e55c222ca02a72bab48a7df47495c137332 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 14 Jul 2021 08:31:01 -0500 Subject: [PATCH 10/67] Make Sample abstract; create MemorySample --- miplearn/features/sample.py | 17 ++++++++++++++++- miplearn/solvers/learning.py | 4 ++-- tests/components/test_dynamic_lazy.py | 8 ++++---- tests/components/test_objective.py | 4 ++-- tests/components/test_primal.py | 4 ++-- tests/components/test_static_lazy.py | 4 ++-- tests/test_features.py | 4 ++-- 7 files changed, 30 insertions(+), 15 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index ae85c72..2fd8e9f 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -2,10 +2,25 @@ # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. +from abc import ABC, abstractmethod from typing import Dict, Optional, Any -class Sample: +class Sample(ABC): + """Abstract dictionary-like class that stores training data.""" + + @abstractmethod + def get(self, key: str) -> Optional[Any]: + pass + + @abstractmethod + def put(self, key: str, value: Any) -> None: + pass + + +class MemorySample(Sample): + """Dictionary-like class that stores training data in-memory.""" + def __init__( self, data: Optional[Dict[str, Any]] = None, diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index 986afdb..a072514 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -15,7 +15,7 @@ from miplearn.components.dynamic_user_cuts import UserCutsComponent from miplearn.components.objective import ObjectiveValueComponent from miplearn.components.primal import PrimalSolutionComponent from miplearn.features.extractor import FeaturesExtractor -from miplearn.features.sample import Sample +from miplearn.features.sample import Sample, MemorySample from miplearn.instance.base import Instance from miplearn.instance.picklegz import PickleGzInstance from miplearn.solvers import _RedirectOutput @@ -150,7 +150,7 @@ class LearningSolver: # Initialize training sample # ------------------------------------------------------- - sample = Sample() + sample = MemorySample() instance.push_sample(sample) # Initialize stats diff --git a/tests/components/test_dynamic_lazy.py b/tests/components/test_dynamic_lazy.py index f29dc47..7e54831 100644 --- a/tests/components/test_dynamic_lazy.py +++ b/tests/components/test_dynamic_lazy.py @@ -11,7 +11,7 @@ from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import MinProbabilityThreshold from miplearn.components import classifier_evaluation_dict from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent -from miplearn.features.sample import Sample +from miplearn.features.sample import Sample, MemorySample from miplearn.instance.base import Instance from miplearn.solvers.tests import assert_equals @@ -22,13 +22,13 @@ E = 0.1 def training_instances() -> List[Instance]: instances = [cast(Instance, Mock(spec=Instance)) for _ in range(2)] samples_0 = [ - Sample( + MemorySample( { "lazy_enforced": {"c1", "c2"}, "instance_features_user": [5.0], }, ), - Sample( + MemorySample( { "lazy_enforced": {"c2", "c3"}, "instance_features_user": [5.0], @@ -53,7 +53,7 @@ def training_instances() -> List[Instance]: } ) samples_1 = [ - Sample( + MemorySample( { "lazy_enforced": {"c3", "c4"}, "instance_features_user": [8.0], diff --git a/tests/components/test_objective.py b/tests/components/test_objective.py index 84f5bc6..57ef271 100644 --- a/tests/components/test_objective.py +++ b/tests/components/test_objective.py @@ -10,14 +10,14 @@ from numpy.testing import assert_array_equal from miplearn.classifiers import Regressor from miplearn.components.objective import ObjectiveValueComponent -from miplearn.features.sample import Sample +from miplearn.features.sample import Sample, MemorySample from miplearn.solvers.learning import LearningSolver from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver @pytest.fixture def sample() -> Sample: - sample = Sample( + sample = MemorySample( { "mip_lower_bound": 1.0, "mip_upper_bound": 2.0, diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index 16b6c2d..e4f2661 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -12,7 +12,7 @@ from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import Threshold from miplearn.components import classifier_evaluation_dict from miplearn.components.primal import PrimalSolutionComponent -from miplearn.features.sample import Sample +from miplearn.features.sample import Sample, MemorySample from miplearn.problems.tsp import TravelingSalesmanGenerator from miplearn.solvers.learning import LearningSolver from miplearn.solvers.tests import assert_equals @@ -20,7 +20,7 @@ from miplearn.solvers.tests import assert_equals @pytest.fixture def sample() -> Sample: - sample = Sample( + sample = MemorySample( { "var_names": ["x[0]", "x[1]", "x[2]", "x[3]"], "var_categories": ["default", None, "default", "default"], diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index ed4f91d..2412eb8 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -11,7 +11,7 @@ from numpy.testing import assert_array_equal from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import Threshold, MinProbabilityThreshold from miplearn.components.static_lazy import StaticLazyConstraintsComponent -from miplearn.features.sample import Sample +from miplearn.features.sample import Sample, MemorySample from miplearn.instance.base import Instance from miplearn.solvers.internal import InternalSolver, Constraints from miplearn.solvers.learning import LearningSolver @@ -22,7 +22,7 @@ from miplearn.types import ( @pytest.fixture def sample() -> Sample: - sample = Sample( + sample = MemorySample( { "constr_categories": [ "type-a", diff --git a/tests/test_features.py b/tests/test_features.py index c7b5557..7c192ea 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -5,7 +5,7 @@ import numpy as np from miplearn.features.extractor import FeaturesExtractor -from miplearn.features.sample import Sample +from miplearn.features.sample import Sample, MemorySample from miplearn.solvers.internal import Variables, Constraints from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.tests import assert_equals @@ -19,7 +19,7 @@ def test_knapsack() -> None: model = instance.to_model() solver.set_instance(instance, model) extractor = FeaturesExtractor() - sample = Sample() + sample = MemorySample() # after-load # ------------------------------------------------------- From 021a71f60ce6e96c1470b1c33764cdc8667da0e5 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 14 Jul 2021 08:39:19 -0500 Subject: [PATCH 11/67] Reorganize feature tests; add basic sample tests --- tests/features/__init__.py | 0 .../test_extractor.py} | 0 tests/features/test_sample.py | 31 +++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 tests/features/__init__.py rename tests/{test_features.py => features/test_extractor.py} (100%) create mode 100644 tests/features/test_sample.py diff --git a/tests/features/__init__.py b/tests/features/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_features.py b/tests/features/test_extractor.py similarity index 100% rename from tests/test_features.py rename to tests/features/test_extractor.py diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py new file mode 100644 index 0000000..5bd869b --- /dev/null +++ b/tests/features/test_sample.py @@ -0,0 +1,31 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +from miplearn.features.sample import MemorySample, Sample + + +def _test_sample(sample: Sample) -> None: + # Strings + sample.put("str", "hello") + assert sample.get("str") == "hello" + + # Numbers + sample.put("int", 1) + sample.put("float", 5.0) + assert sample.get("int") == 1 + assert sample.get("float") == 5.0 + + # List of strings + sample.put("strlist", ["hello", "world"]) + assert sample.get("strlist") == ["hello", "world"] + + # List of numbers + sample.put("intlist", [1, 2, 3]) + sample.put("floatlist", [4.0, 5.0, 6.0]) + assert sample.get("intlist") == [1, 2, 3] + assert sample.get("floatlist") == [4.0, 5.0, 6.0] + + +def test_memory_sample() -> None: + _test_sample(MemorySample()) From 0a399deeee44341e5cedf786a6c0ac6204756405 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 14 Jul 2021 09:56:25 -0500 Subject: [PATCH 12/67] Implement Hdf5Sample --- miplearn/features/sample.py | 61 +++++++++++++++++++++++++++++++++++ tests/features/test_sample.py | 53 ++++++++++++++++++------------ 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 2fd8e9f..bd23d9b 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -5,6 +5,10 @@ from abc import ABC, abstractmethod from typing import Dict, Optional, Any +import h5py +import numpy as np +from overrides import overrides + class Sample(ABC): """Abstract dictionary-like class that stores training data.""" @@ -15,8 +19,29 @@ class Sample(ABC): @abstractmethod def put(self, key: str, value: Any) -> None: + """ + Add a new key/value pair to the sample. If the key already exists, + the previous value is silently replaced. + + Only the following data types are supported: + - str, bool, int, float + - List[str], List[bool], List[int], List[float] + """ pass + def _assert_supported(self, value: Any) -> None: + def _is_primitive(v: Any) -> bool: + if isinstance(v, (str, bool, int, float)): + return True + return False + + if _is_primitive(value): + return + if isinstance(value, list): + if _is_primitive(value[0]): + return + assert False, f"Value has unsupported type: {value}" + class MemorySample(Sample): """Dictionary-like class that stores training data in-memory.""" @@ -29,11 +54,47 @@ class MemorySample(Sample): data = {} self._data: Dict[str, Any] = data + @overrides def get(self, key: str) -> Optional[Any]: if key in self._data: return self._data[key] else: return None + @overrides def put(self, key: str, value: Any) -> None: + # self._assert_supported(value) self._data[key] = value + + +class Hdf5Sample(Sample): + """ + Dictionary-like class that stores training data in an HDF5 file. + + Unlike MemorySample, this class only loads to memory the parts of the data set that + are actually accessed, and therefore it is more scalable. + """ + + def __init__(self, filename: str) -> None: + self.file = h5py.File(filename, "r+") + + @overrides + def get(self, key: str) -> Optional[Any]: + ds = self.file[key] + if h5py.check_string_dtype(ds.dtype): + if ds.shape == (): + return ds.asstr()[()] + else: + return ds.asstr()[:].tolist() + else: + if ds.shape == (): + return ds[()].tolist() + else: + return ds[:].tolist() + + @overrides + def put(self, key: str, value: Any) -> None: + self._assert_supported(value) + if key in self.file: + del self.file[key] + self.file.create_dataset(key, data=value) diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index 5bd869b..3cdb5e7 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -1,31 +1,44 @@ # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. +from tempfile import NamedTemporaryFile +from typing import Any -from miplearn.features.sample import MemorySample, Sample +from miplearn.features.sample import MemorySample, Sample, Hdf5Sample def _test_sample(sample: Sample) -> None: - # Strings - sample.put("str", "hello") - assert sample.get("str") == "hello" - - # Numbers - sample.put("int", 1) - sample.put("float", 5.0) - assert sample.get("int") == 1 - assert sample.get("float") == 5.0 - - # List of strings - sample.put("strlist", ["hello", "world"]) - assert sample.get("strlist") == ["hello", "world"] - - # List of numbers - sample.put("intlist", [1, 2, 3]) - sample.put("floatlist", [4.0, 5.0, 6.0]) - assert sample.get("intlist") == [1, 2, 3] - assert sample.get("floatlist") == [4.0, 5.0, 6.0] + _assert_roundtrip(sample, "A") + _assert_roundtrip(sample, True) + _assert_roundtrip(sample, 1) + _assert_roundtrip(sample, 1.0) + _assert_roundtrip(sample, ["A", "BB", "CCC", "こんにちは"]) + _assert_roundtrip(sample, [True, True, False]) + _assert_roundtrip(sample, [1, 2, 3]) + _assert_roundtrip(sample, [1.0, 2.0, 3.0]) + + +def _assert_roundtrip(sample: Sample, expected: Any) -> None: + sample.put("key", expected) + actual = sample.get("key") + assert actual == expected + assert actual is not None + if isinstance(actual, list): + assert isinstance(actual[0], expected[0].__class__), ( + f"Expected class {expected[0].__class__}, " + f"found {actual[0].__class__} instead" + ) + else: + assert isinstance(actual, expected.__class__), ( + f"Expected class {expected.__class__}, " + f"found class {actual.__class__} instead" + ) def test_memory_sample() -> None: _test_sample(MemorySample()) + + +def test_hdf5_sample() -> None: + file = NamedTemporaryFile() + _test_sample(Hdf5Sample(file.name)) From 8fc7c6ab71493c181eb4efc10d2d58e43d786eb4 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 14 Jul 2021 10:50:54 -0500 Subject: [PATCH 13/67] Split Sample.{get,put} into {get,put}_{scalar,vector} --- miplearn/features/extractor.py | 52 ++++++++--------- miplearn/features/sample.py | 101 +++++++++++++++++++++++++++++---- miplearn/solvers/learning.py | 4 +- tests/features/test_sample.py | 76 ++++++++++++++++--------- 4 files changed, 165 insertions(+), 68 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index b26f97f..6648f6d 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -33,15 +33,15 @@ class FeaturesExtractor: ) -> None: variables = solver.get_variables(with_static=True) constraints = solver.get_constraints(with_static=True, with_lhs=self.with_lhs) - sample.put("var_lower_bounds", variables.lower_bounds) - sample.put("var_names", variables.names) - sample.put("var_obj_coeffs", variables.obj_coeffs) - sample.put("var_types", variables.types) - sample.put("var_upper_bounds", variables.upper_bounds) - sample.put("constr_names", constraints.names) + sample.put_vector("var_lower_bounds", variables.lower_bounds) + sample.put_vector("var_names", variables.names) + sample.put_vector("var_obj_coeffs", variables.obj_coeffs) + sample.put_vector("var_types", variables.types) + sample.put_vector("var_upper_bounds", variables.upper_bounds) + sample.put_vector("constr_names", constraints.names) sample.put("constr_lhs", constraints.lhs) - sample.put("constr_rhs", constraints.rhs) - sample.put("constr_senses", constraints.senses) + sample.put_vector("constr_rhs", constraints.rhs) + sample.put_vector("constr_senses", constraints.senses) self._extract_user_features_vars(instance, sample) self._extract_user_features_constrs(instance, sample) self._extract_user_features_instance(instance, sample) @@ -67,20 +67,20 @@ class FeaturesExtractor: ) -> None: variables = solver.get_variables(with_static=False, with_sa=self.with_sa) constraints = solver.get_constraints(with_static=False, with_sa=self.with_sa) - sample.put("lp_var_basis_status", variables.basis_status) - sample.put("lp_var_reduced_costs", variables.reduced_costs) - sample.put("lp_var_sa_lb_down", variables.sa_lb_down) - sample.put("lp_var_sa_lb_up", variables.sa_lb_up) - sample.put("lp_var_sa_obj_down", variables.sa_obj_down) - sample.put("lp_var_sa_obj_up", variables.sa_obj_up) - sample.put("lp_var_sa_ub_down", variables.sa_ub_down) - sample.put("lp_var_sa_ub_up", variables.sa_ub_up) - sample.put("lp_var_values", variables.values) - sample.put("lp_constr_basis_status", constraints.basis_status) - sample.put("lp_constr_dual_values", constraints.dual_values) - sample.put("lp_constr_sa_rhs_down", constraints.sa_rhs_down) - sample.put("lp_constr_sa_rhs_up", constraints.sa_rhs_up) - sample.put("lp_constr_slacks", constraints.slacks) + sample.put_vector("lp_var_basis_status", variables.basis_status) + sample.put_vector("lp_var_reduced_costs", variables.reduced_costs) + sample.put_vector("lp_var_sa_lb_down", variables.sa_lb_down) + sample.put_vector("lp_var_sa_lb_up", variables.sa_lb_up) + sample.put_vector("lp_var_sa_obj_down", variables.sa_obj_down) + sample.put_vector("lp_var_sa_obj_up", variables.sa_obj_up) + sample.put_vector("lp_var_sa_ub_down", variables.sa_ub_down) + sample.put_vector("lp_var_sa_ub_up", variables.sa_ub_up) + sample.put_vector("lp_var_values", variables.values) + sample.put_vector("lp_constr_basis_status", constraints.basis_status) + sample.put_vector("lp_constr_dual_values", constraints.dual_values) + sample.put_vector("lp_constr_sa_rhs_down", constraints.sa_rhs_down) + sample.put_vector("lp_constr_sa_rhs_up", constraints.sa_rhs_up) + sample.put_vector("lp_constr_slacks", constraints.slacks) self._extract_var_features_AlvLouWeh2017(sample, prefix="lp_") sample.put( "lp_var_features", @@ -134,8 +134,8 @@ class FeaturesExtractor: ) -> None: variables = solver.get_variables(with_static=False, with_sa=False) constraints = solver.get_constraints(with_static=False, with_sa=False) - sample.put("mip_var_values", variables.values) - sample.put("mip_constr_slacks", constraints.slacks) + sample.put_vector("mip_var_values", variables.values) + sample.put_vector("mip_constr_slacks", constraints.slacks) def _extract_user_features_vars( self, @@ -228,7 +228,7 @@ class FeaturesExtractor: else: lazy.append(False) sample.put("constr_features_user", user_features) - sample.put("constr_lazy", lazy) + sample.put_vector("constr_lazy", lazy) sample.put("constr_categories", categories) def _extract_user_features_instance( @@ -251,7 +251,7 @@ class FeaturesExtractor: constr_lazy = sample.get("constr_lazy") assert constr_lazy is not None sample.put("instance_features_user", user_features) - sample.put("static_lazy_count", sum(constr_lazy)) + sample.put_scalar("static_lazy_count", sum(constr_lazy)) # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based # approximation of strong branching. INFORMS Journal on Computing, 29(1), 185-195. diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index bd23d9b..27810c3 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -3,16 +3,34 @@ # Released under the modified BSD license. See COPYING.md for more details. from abc import ABC, abstractmethod -from typing import Dict, Optional, Any +from typing import Dict, Optional, Any, Union, List import h5py -import numpy as np from overrides import overrides +Scalar = Union[None, bool, str, int, float] +Vector = Union[None, List[bool], List[str], List[int], List[float]] + class Sample(ABC): """Abstract dictionary-like class that stores training data.""" + @abstractmethod + def get_scalar(self, key: str) -> Optional[Any]: + pass + + @abstractmethod + def put_scalar(self, key: str, value: Scalar) -> None: + pass + + @abstractmethod + def get_vector(self, key: str) -> Optional[Any]: + pass + + @abstractmethod + def put_vector(self, key: str, value: Vector) -> None: + pass + @abstractmethod def get(self, key: str) -> Optional[Any]: pass @@ -33,6 +51,8 @@ class Sample(ABC): def _is_primitive(v: Any) -> bool: if isinstance(v, (str, bool, int, float)): return True + if v is None: + return True return False if _is_primitive(value): @@ -40,8 +60,23 @@ class Sample(ABC): if isinstance(value, list): if _is_primitive(value[0]): return + if isinstance(value[0], list): + if _is_primitive(value[0][0]): + return assert False, f"Value has unsupported type: {value}" + def _assert_scalar(self, value: Any) -> None: + if value is None: + return + if isinstance(value, (str, bool, int, float)): + return + assert False, f"Scalar expected; found instead: {value}" + + def _assert_vector(self, value: Any) -> None: + assert isinstance(value, list), f"List expected; found instead: {value}" + for v in value: + self._assert_scalar(v) + class MemorySample(Sample): """Dictionary-like class that stores training data in-memory.""" @@ -54,6 +89,26 @@ class MemorySample(Sample): data = {} self._data: Dict[str, Any] = data + @overrides + def get_scalar(self, key: str) -> Optional[Any]: + return self.get(key) + + @overrides + def put_scalar(self, key: str, value: Scalar) -> None: + self._assert_scalar(value) + self.put(key, value) + + @overrides + def get_vector(self, key: str) -> Optional[Any]: + return self.get(key) + + @overrides + def put_vector(self, key: str, value: Vector) -> None: + if value is None: + return + self._assert_vector(value) + self.put(key, value) + @overrides def get(self, key: str) -> Optional[Any]: if key in self._data: @@ -63,7 +118,6 @@ class MemorySample(Sample): @overrides def put(self, key: str, value: Any) -> None: - # self._assert_supported(value) self._data[key] = value @@ -78,23 +132,46 @@ class Hdf5Sample(Sample): def __init__(self, filename: str) -> None: self.file = h5py.File(filename, "r+") + @overrides + def get_scalar(self, key: str) -> Optional[Any]: + ds = self.file[key] + assert len(ds.shape) == 0 + if h5py.check_string_dtype(ds.dtype): + return ds.asstr()[()] + else: + return ds[()].tolist() + + @overrides + def get_vector(self, key: str) -> Optional[Any]: + ds = self.file[key] + assert len(ds.shape) == 1 + if h5py.check_string_dtype(ds.dtype): + return ds.asstr()[:].tolist() + else: + return ds[:].tolist() + + @overrides + def put_scalar(self, key: str, value: Any) -> None: + self._assert_scalar(value) + self.put(key, value) + + @overrides + def put_vector(self, key: str, value: Vector) -> None: + if value is None: + return + self._assert_vector(value) + self.put(key, value) + @overrides def get(self, key: str) -> Optional[Any]: ds = self.file[key] if h5py.check_string_dtype(ds.dtype): - if ds.shape == (): - return ds.asstr()[()] - else: - return ds.asstr()[:].tolist() + return ds.asstr()[:].tolist() else: - if ds.shape == (): - return ds[()].tolist() - else: - return ds[:].tolist() + return ds[:].tolist() @overrides def put(self, key: str, value: Any) -> None: - self._assert_supported(value) if key in self.file: del self.file[key] self.file.create_dataset(key, data=value) diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index a072514..efb10b7 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -210,7 +210,7 @@ class LearningSolver: logger.info("Extracting features (after-lp)...") initial_time = time.time() for (k, v) in lp_stats.__dict__.items(): - sample.put(k, v) + sample.put_scalar(k, v) self.extractor.extract_after_lp_features(self.internal_solver, sample) logger.info( "Features (after-lp) extracted in %.2f seconds" @@ -278,7 +278,7 @@ class LearningSolver: logger.info("Extracting features (after-mip)...") initial_time = time.time() for (k, v) in mip_stats.__dict__.items(): - sample.put(k, v) + sample.put_scalar(k, v) self.extractor.extract_after_mip_features(self.internal_solver, sample) logger.info( "Features (after-mip) extracted in %.2f seconds" diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index 3cdb5e7..64b74c8 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -7,34 +7,6 @@ from typing import Any from miplearn.features.sample import MemorySample, Sample, Hdf5Sample -def _test_sample(sample: Sample) -> None: - _assert_roundtrip(sample, "A") - _assert_roundtrip(sample, True) - _assert_roundtrip(sample, 1) - _assert_roundtrip(sample, 1.0) - _assert_roundtrip(sample, ["A", "BB", "CCC", "こんにちは"]) - _assert_roundtrip(sample, [True, True, False]) - _assert_roundtrip(sample, [1, 2, 3]) - _assert_roundtrip(sample, [1.0, 2.0, 3.0]) - - -def _assert_roundtrip(sample: Sample, expected: Any) -> None: - sample.put("key", expected) - actual = sample.get("key") - assert actual == expected - assert actual is not None - if isinstance(actual, list): - assert isinstance(actual[0], expected[0].__class__), ( - f"Expected class {expected[0].__class__}, " - f"found {actual[0].__class__} instead" - ) - else: - assert isinstance(actual, expected.__class__), ( - f"Expected class {expected.__class__}, " - f"found class {actual.__class__} instead" - ) - - def test_memory_sample() -> None: _test_sample(MemorySample()) @@ -42,3 +14,51 @@ def test_memory_sample() -> None: def test_hdf5_sample() -> None: file = NamedTemporaryFile() _test_sample(Hdf5Sample(file.name)) + + +def _test_sample(sample: Sample) -> None: + # Scalar + _assert_roundtrip_scalar(sample, "A") + _assert_roundtrip_scalar(sample, True) + _assert_roundtrip_scalar(sample, 1) + _assert_roundtrip_scalar(sample, 1.0) + + # Vector + _assert_roundtrip_vector(sample, ["A", "BB", "CCC", "こんにちは"]) + _assert_roundtrip_vector(sample, [True, True, False]) + _assert_roundtrip_vector(sample, [1, 2, 3]) + _assert_roundtrip_vector(sample, [1.0, 2.0, 3.0]) + + # List[Optional[List[Primitive]]] + # _assert_roundtrip( + # sample, + # [ + # [1], + # None, + # [2, 2], + # [3, 3, 3], + # ], + # ) + + +def _assert_roundtrip_scalar(sample: Sample, expected: Any) -> None: + sample.put_scalar("key", expected) + actual = sample.get_scalar("key") + assert actual == expected + assert actual is not None + _assert_same_type(actual, expected) + + +def _assert_roundtrip_vector(sample: Sample, expected: Any) -> None: + sample.put_vector("key", expected) + actual = sample.get_vector("key") + assert actual == expected + assert actual is not None + _assert_same_type(actual[0], expected[0]) + + +def _assert_same_type(actual: Any, expected: Any) -> None: + assert isinstance(actual, expected.__class__), ( + f"Expected class {expected.__class__}, " + f"found class {actual.__class__} instead" + ) From 8d89285cb994c439dd8c50b83277ad417be6e96d Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 14 Jul 2021 12:21:09 -0500 Subject: [PATCH 14/67] Implement {get,put}_vector_list --- miplearn/features/extractor.py | 16 ++-- miplearn/features/sample.py | 132 ++++++++++++++++++++++++++++++--- tests/features/test_sample.py | 91 +++++++++++++++++++---- 3 files changed, 206 insertions(+), 33 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 6648f6d..a7f2b11 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -46,7 +46,7 @@ class FeaturesExtractor: self._extract_user_features_constrs(instance, sample) self._extract_user_features_instance(instance, sample) self._extract_var_features_AlvLouWeh2017(sample) - sample.put( + sample.put_vector_list( "var_features", self._combine( sample, @@ -82,7 +82,7 @@ class FeaturesExtractor: sample.put_vector("lp_constr_sa_rhs_up", constraints.sa_rhs_up) sample.put_vector("lp_constr_slacks", constraints.slacks) self._extract_var_features_AlvLouWeh2017(sample, prefix="lp_") - sample.put( + sample.put_vector_list( "lp_var_features", self._combine( sample, @@ -103,7 +103,7 @@ class FeaturesExtractor: ], ), ) - sample.put( + sample.put_vector_list( "lp_constr_features", self._combine( sample, @@ -118,7 +118,7 @@ class FeaturesExtractor: ) instance_features_user = sample.get("instance_features_user") assert instance_features_user is not None - sample.put( + sample.put_vector( "lp_instance_features", instance_features_user + [ @@ -178,7 +178,7 @@ class FeaturesExtractor: user_features_i = list(user_features_i) user_features.append(user_features_i) sample.put("var_categories", categories) - sample.put("var_features_user", user_features) + sample.put_vector_list("var_features_user", user_features) def _extract_user_features_constrs( self, @@ -227,7 +227,7 @@ class FeaturesExtractor: lazy.append(instance.is_constraint_lazy(cname)) else: lazy.append(False) - sample.put("constr_features_user", user_features) + sample.put_vector_list("constr_features_user", user_features) sample.put_vector("constr_lazy", lazy) sample.put("constr_categories", categories) @@ -250,7 +250,7 @@ class FeaturesExtractor: ) constr_lazy = sample.get("constr_lazy") assert constr_lazy is not None - sample.put("instance_features_user", user_features) + sample.put_vector("instance_features_user", user_features) sample.put_scalar("static_lazy_count", sum(constr_lazy)) # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based @@ -331,7 +331,7 @@ class FeaturesExtractor: for v in f: assert isfinite(v), f"non-finite elements detected: {f}" features.append(f) - sample.put(f"{prefix}var_features_AlvLouWeh2017", features) + sample.put_vector_list(f"{prefix}var_features_AlvLouWeh2017", features) def _combine( self, diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 27810c3..6d4f131 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -3,13 +3,25 @@ # Released under the modified BSD license. See COPYING.md for more details. from abc import ABC, abstractmethod -from typing import Dict, Optional, Any, Union, List +from copy import deepcopy +from typing import Dict, Optional, Any, Union, List, Tuple, cast import h5py +import numpy as np from overrides import overrides Scalar = Union[None, bool, str, int, float] Vector = Union[None, List[bool], List[str], List[int], List[float]] +VectorList = Union[ + List[List[bool]], + List[List[str]], + List[List[int]], + List[List[float]], + List[Optional[List[bool]]], + List[Optional[List[str]]], + List[Optional[List[int]]], + List[Optional[List[float]]], +] class Sample(ABC): @@ -31,6 +43,14 @@ class Sample(ABC): def put_vector(self, key: str, value: Vector) -> None: pass + @abstractmethod + def get_vector_list(self, key: str) -> Optional[Any]: + pass + + @abstractmethod + def put_vector_list(self, key: str, value: VectorList) -> None: + pass + @abstractmethod def get(self, key: str) -> Optional[Any]: pass @@ -65,17 +85,24 @@ class Sample(ABC): return assert False, f"Value has unsupported type: {value}" - def _assert_scalar(self, value: Any) -> None: + def _assert_is_scalar(self, value: Any) -> None: if value is None: return if isinstance(value, (str, bool, int, float)): return assert False, f"Scalar expected; found instead: {value}" - def _assert_vector(self, value: Any) -> None: + def _assert_is_vector(self, value: Any) -> None: assert isinstance(value, list), f"List expected; found instead: {value}" for v in value: - self._assert_scalar(v) + self._assert_is_scalar(v) + + def _assert_is_vector_list(self, value: Any) -> None: + assert isinstance(value, list), f"List expected; found instead: {value}" + for v in value: + if v is None: + continue + self._assert_is_vector(v) class MemorySample(Sample): @@ -94,19 +121,28 @@ class MemorySample(Sample): return self.get(key) @overrides - def put_scalar(self, key: str, value: Scalar) -> None: - self._assert_scalar(value) - self.put(key, value) + def get_vector(self, key: str) -> Optional[Any]: + return self.get(key) @overrides - def get_vector(self, key: str) -> Optional[Any]: + def get_vector_list(self, key: str) -> Optional[Any]: return self.get(key) + @overrides + def put_scalar(self, key: str, value: Scalar) -> None: + self._assert_is_scalar(value) + self.put(key, value) + @overrides def put_vector(self, key: str, value: Vector) -> None: if value is None: return - self._assert_vector(value) + self._assert_is_vector(value) + self.put(key, value) + + @overrides + def put_vector_list(self, key: str, value: VectorList) -> None: + self._assert_is_vector_list(value) self.put(key, value) @overrides @@ -145,23 +181,55 @@ class Hdf5Sample(Sample): def get_vector(self, key: str) -> Optional[Any]: ds = self.file[key] assert len(ds.shape) == 1 + print(ds.dtype) if h5py.check_string_dtype(ds.dtype): return ds.asstr()[:].tolist() else: return ds[:].tolist() + @overrides + def get_vector_list(self, key: str) -> Optional[Any]: + ds = self.file[key] + lens = ds.attrs["lengths"] + if h5py.check_string_dtype(ds.dtype): + padded = ds.asstr()[:].tolist() + else: + padded = ds[:].tolist() + return _crop(padded, lens) + @overrides def put_scalar(self, key: str, value: Any) -> None: - self._assert_scalar(value) + self._assert_is_scalar(value) self.put(key, value) @overrides def put_vector(self, key: str, value: Vector) -> None: if value is None: return - self._assert_vector(value) + self._assert_is_vector(value) self.put(key, value) + @overrides + def put_vector_list(self, key: str, value: VectorList) -> None: + self._assert_is_vector_list(value) + if key in self.file: + del self.file[key] + padded, lens = _pad(value) + data = None + for v in value: + if v is None or len(v) == 0: + continue + if isinstance(v[0], str): + data = np.array(padded, dtype="S") + elif isinstance(v[0], bool): + data = np.array(padded, dtype=bool) + else: + data = np.array(padded) + break + assert data is not None + ds = self.file.create_dataset(key, data=data) + ds.attrs["lengths"] = lens + @overrides def get(self, key: str) -> Optional[Any]: ds = self.file[key] @@ -175,3 +243,45 @@ class Hdf5Sample(Sample): if key in self.file: del self.file[key] self.file.create_dataset(key, data=value) + + +def _pad(veclist: VectorList) -> Tuple[VectorList, List[int]]: + veclist = deepcopy(veclist) + lens = [len(v) if v is not None else -1 for v in veclist] + maxlen = max(lens) + + # Find appropriate constant to pad the vectors + constant: Union[int, float, str, None] = None + for v in veclist: + if v is None or len(v) == 0: + continue + if isinstance(v[0], int): + constant = 0 + elif isinstance(v[0], float): + constant = 0.0 + elif isinstance(v[0], str): + constant = "" + else: + assert False, f"Unsupported data type: {v[0]}" + assert constant is not None, "veclist must not be completely empty" + + # Pad vectors + for (i, vi) in enumerate(veclist): + if vi is None: + vi = veclist[i] = [] + assert isinstance(vi, list) + for k in range(len(vi), maxlen): + vi.append(constant) + + return veclist, lens + + +def _crop(veclist: VectorList, lens: List[int]) -> VectorList: + result: VectorList = cast(VectorList, []) + for (i, v) in enumerate(veclist): + if lens[i] < 0: + result.append(None) # type: ignore + else: + assert isinstance(v, list) + result.append(v[: lens[i]]) + return result diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index 64b74c8..5462210 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -4,7 +4,7 @@ from tempfile import NamedTemporaryFile from typing import Any -from miplearn.features.sample import MemorySample, Sample, Hdf5Sample +from miplearn.features.sample import MemorySample, Sample, Hdf5Sample, _pad, _crop def test_memory_sample() -> None: @@ -29,16 +29,11 @@ def _test_sample(sample: Sample) -> None: _assert_roundtrip_vector(sample, [1, 2, 3]) _assert_roundtrip_vector(sample, [1.0, 2.0, 3.0]) - # List[Optional[List[Primitive]]] - # _assert_roundtrip( - # sample, - # [ - # [1], - # None, - # [2, 2], - # [3, 3, 3], - # ], - # ) + # VectorList + _assert_roundtrip_vector_list(sample, [["A"], ["BB", "CCC"], None]) + _assert_roundtrip_vector_list(sample, [[True], [False, False], None]) + _assert_roundtrip_vector_list(sample, [[1], None, [2, 2], [3, 3, 3]]) + _assert_roundtrip_vector_list(sample, [[1.0], None, [2.0, 2.0], [3.0, 3.0, 3.0]]) def _assert_roundtrip_scalar(sample: Sample, expected: Any) -> None: @@ -57,8 +52,76 @@ def _assert_roundtrip_vector(sample: Sample, expected: Any) -> None: _assert_same_type(actual[0], expected[0]) +def _assert_roundtrip_vector_list(sample: Sample, expected: Any) -> None: + sample.put_vector_list("key", expected) + actual = sample.get_vector_list("key") + assert actual == expected + assert actual is not None + _assert_same_type(actual[0][0], expected[0][0]) + + def _assert_same_type(actual: Any, expected: Any) -> None: - assert isinstance(actual, expected.__class__), ( - f"Expected class {expected.__class__}, " - f"found class {actual.__class__} instead" + assert isinstance( + actual, expected.__class__ + ), f"Expected {expected.__class__}, found {actual.__class__} instead" + + +def test_pad_int() -> None: + _assert_roundtrip_pad( + original=[[1], [2, 2, 2], [], [3, 3], [4, 4, 4, 4], None], + expected_padded=[ + [1, 0, 0, 0], + [2, 2, 2, 0], + [0, 0, 0, 0], + [3, 3, 0, 0], + [4, 4, 4, 4], + [0, 0, 0, 0], + ], + expected_lens=[1, 3, 0, 2, 4, -1], + dtype=int, + ) + + +def test_pad_float() -> None: + _assert_roundtrip_pad( + original=[[1.0], [2.0, 2.0, 2.0], [3.0, 3.0], [4.0, 4.0, 4.0, 4.0], None], + expected_padded=[ + [1.0, 0.0, 0.0, 0.0], + [2.0, 2.0, 2.0, 0.0], + [3.0, 3.0, 0.0, 0.0], + [4.0, 4.0, 4.0, 4.0], + [0.0, 0.0, 0.0, 0.0], + ], + expected_lens=[1, 3, 2, 4, -1], + dtype=float, ) + + +def test_pad_str() -> None: + _assert_roundtrip_pad( + original=[["A"], ["B", "B", "B"], ["C", "C"]], + expected_padded=[["A", "", ""], ["B", "B", "B"], ["C", "C", ""]], + expected_lens=[1, 3, 2], + dtype=str, + ) + + +def _assert_roundtrip_pad( + original: Any, + expected_padded: Any, + expected_lens: Any, + dtype: Any, +) -> None: + actual_padded, actual_lens = _pad(original) + assert actual_padded == expected_padded + assert actual_lens == expected_lens + for v in actual_padded: + for vi in v: # type: ignore + assert isinstance(vi, dtype) + cropped = _crop(actual_padded, actual_lens) + assert cropped == original + for v in cropped: + if v is None: + continue + for vi in v: # type: ignore + assert isinstance(vi, dtype) From ef9c48d79a65f46c26ccf8f94efb38e11774fdde Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 15 Jul 2021 16:21:40 -0500 Subject: [PATCH 15/67] Replace Hashable by str --- CHANGELOG.md | 1 + miplearn/components/component.py | 8 ++--- miplearn/components/dynamic_common.py | 36 +++++++++++----------- miplearn/components/dynamic_lazy.py | 14 ++++----- miplearn/components/dynamic_user_cuts.py | 14 ++++----- miplearn/components/objective.py | 16 +++++----- miplearn/components/primal.py | 26 ++++++---------- miplearn/components/static_lazy.py | 30 +++++++++--------- miplearn/features/extractor.py | 18 +++++------ miplearn/instance/base.py | 23 +++++++------- miplearn/instance/picklegz.py | 14 ++++----- miplearn/problems/knapsack.py | 4 +-- miplearn/problems/stab.py | 5 ++- miplearn/problems/tsp.py | 7 ++--- miplearn/solvers/gurobi.py | 4 +-- miplearn/solvers/pyomo/base.py | 4 +-- miplearn/types.py | 4 +-- tests/components/test_dynamic_user_cuts.py | 12 ++++---- tests/components/test_objective.py | 6 ++-- tests/components/test_primal.py | 4 +-- tests/components/test_static_lazy.py | 6 ++-- 21 files changed, 123 insertions(+), 133 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a8c299..588686c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ ) ``` - `LazyConstraintComponent` has been renamed to `DynamicLazyConstraintsComponent`. +- Categories, lazy constraints and cutting plane identifiers must now be strings, instead `Hashable`. This change was required for compatibility with HDF5 data format. ### Removed diff --git a/miplearn/components/component.py b/miplearn/components/component.py index 22a1cda..3013d4e 100644 --- a/miplearn/components/component.py +++ b/miplearn/components/component.py @@ -2,7 +2,7 @@ # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from typing import Any, List, TYPE_CHECKING, Tuple, Dict, Hashable, Optional +from typing import Any, List, TYPE_CHECKING, Tuple, Dict, Optional import numpy as np from p_tqdm import p_umap @@ -101,8 +101,8 @@ class Component: def fit_xy( self, - x: Dict[Hashable, np.ndarray], - y: Dict[Hashable, np.ndarray], + x: Dict[str, np.ndarray], + y: Dict[str, np.ndarray], ) -> None: """ Given two dictionaries x and y, mapping the name of the category to matrices @@ -152,7 +152,7 @@ class Component: self, instance: Optional[Instance], sample: Sample, - ) -> Dict[Hashable, Dict[str, float]]: + ) -> Dict[str, Dict[str, float]]: return {} def sample_xy( diff --git a/miplearn/components/dynamic_common.py b/miplearn/components/dynamic_common.py index 3f69156..24b375d 100644 --- a/miplearn/components/dynamic_common.py +++ b/miplearn/components/dynamic_common.py @@ -3,7 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging -from typing import Dict, Hashable, List, Tuple, Optional, Any, Set +from typing import Dict, List, Tuple, Optional, Any, Set import numpy as np from overrides import overrides @@ -32,8 +32,8 @@ class DynamicConstraintsComponent(Component): assert isinstance(classifier, Classifier) self.threshold_prototype: Threshold = threshold self.classifier_prototype: Classifier = classifier - self.classifiers: Dict[Hashable, Classifier] = {} - self.thresholds: Dict[Hashable, Threshold] = {} + self.classifiers: Dict[str, Classifier] = {} + self.thresholds: Dict[str, Threshold] = {} self.known_cids: List[str] = [] self.attr = attr @@ -42,14 +42,14 @@ class DynamicConstraintsComponent(Component): instance: Optional[Instance], sample: Sample, ) -> Tuple[ - Dict[Hashable, List[List[float]]], - Dict[Hashable, List[List[bool]]], - Dict[Hashable, List[str]], + Dict[str, List[List[float]]], + Dict[str, List[List[bool]]], + Dict[str, List[str]], ]: assert instance is not None - x: Dict[Hashable, List[List[float]]] = {} - y: Dict[Hashable, List[List[bool]]] = {} - cids: Dict[Hashable, List[str]] = {} + x: Dict[str, List[List[float]]] = {} + y: Dict[str, List[List[bool]]] = {} + cids: Dict[str, List[str]] = {} constr_categories_dict = instance.get_constraint_categories() constr_features_dict = instance.get_constraint_features() instance_features = sample.get("instance_features_user") @@ -111,8 +111,8 @@ class DynamicConstraintsComponent(Component): self, instance: Instance, sample: Sample, - ) -> List[Hashable]: - pred: List[Hashable] = [] + ) -> List[str]: + pred: List[str] = [] if len(self.known_cids) == 0: logger.info("Classifiers not fitted. Skipping.") return pred @@ -137,8 +137,8 @@ class DynamicConstraintsComponent(Component): @overrides def fit_xy( self, - x: Dict[Hashable, np.ndarray], - y: Dict[Hashable, np.ndarray], + x: Dict[str, np.ndarray], + y: Dict[str, np.ndarray], ) -> None: for category in x.keys(): self.classifiers[category] = self.classifier_prototype.clone() @@ -153,14 +153,14 @@ class DynamicConstraintsComponent(Component): self, instance: Instance, sample: Sample, - ) -> Dict[Hashable, Dict[str, float]]: + ) -> Dict[str, Dict[str, float]]: actual = sample.get(self.attr) assert actual is not None pred = set(self.sample_predict(instance, sample)) - tp: Dict[Hashable, int] = {} - tn: Dict[Hashable, int] = {} - fp: Dict[Hashable, int] = {} - fn: Dict[Hashable, int] = {} + tp: Dict[str, int] = {} + tn: Dict[str, int] = {} + fp: Dict[str, int] = {} + fn: Dict[str, int] = {} constr_categories_dict = instance.get_constraint_categories() for cid in self.known_cids: if cid not in constr_categories_dict: diff --git a/miplearn/components/dynamic_lazy.py b/miplearn/components/dynamic_lazy.py index fdf56aa..1ccfc21 100644 --- a/miplearn/components/dynamic_lazy.py +++ b/miplearn/components/dynamic_lazy.py @@ -3,7 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging -from typing import Dict, List, TYPE_CHECKING, Hashable, Tuple, Any, Optional, Set +from typing import Dict, List, TYPE_CHECKING, Tuple, Any, Optional, Set import numpy as np from overrides import overrides @@ -41,11 +41,11 @@ class DynamicLazyConstraintsComponent(Component): self.classifiers = self.dynamic.classifiers self.thresholds = self.dynamic.thresholds self.known_cids = self.dynamic.known_cids - self.lazy_enforced: Set[Hashable] = set() + self.lazy_enforced: Set[str] = set() @staticmethod def enforce( - cids: List[Hashable], + cids: List[str], instance: Instance, model: Any, solver: "LearningSolver", @@ -117,7 +117,7 @@ class DynamicLazyConstraintsComponent(Component): self, instance: Instance, sample: Sample, - ) -> List[Hashable]: + ) -> List[str]: return self.dynamic.sample_predict(instance, sample) @overrides @@ -127,8 +127,8 @@ class DynamicLazyConstraintsComponent(Component): @overrides def fit_xy( self, - x: Dict[Hashable, np.ndarray], - y: Dict[Hashable, np.ndarray], + x: Dict[str, np.ndarray], + y: Dict[str, np.ndarray], ) -> None: self.dynamic.fit_xy(x, y) @@ -137,5 +137,5 @@ class DynamicLazyConstraintsComponent(Component): self, instance: Instance, sample: Sample, - ) -> Dict[Hashable, Dict[str, float]]: + ) -> Dict[str, Dict[str, float]]: return self.dynamic.sample_evaluate(instance, sample) diff --git a/miplearn/components/dynamic_user_cuts.py b/miplearn/components/dynamic_user_cuts.py index 4db64e0..c468c08 100644 --- a/miplearn/components/dynamic_user_cuts.py +++ b/miplearn/components/dynamic_user_cuts.py @@ -3,7 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging -from typing import Any, TYPE_CHECKING, Hashable, Set, Tuple, Dict, List, Optional +from typing import Any, TYPE_CHECKING, Set, Tuple, Dict, List, Optional import numpy as np from overrides import overrides @@ -34,7 +34,7 @@ class UserCutsComponent(Component): threshold=threshold, attr="user_cuts_enforced", ) - self.enforced: Set[Hashable] = set() + self.enforced: Set[str] = set() self.n_added_in_callback = 0 @overrides @@ -71,7 +71,7 @@ class UserCutsComponent(Component): for cid in cids: if cid in self.enforced: continue - assert isinstance(cid, Hashable) + assert isinstance(cid, str) instance.enforce_user_cut(solver.internal_solver, model, cid) self.enforced.add(cid) self.n_added_in_callback += 1 @@ -110,7 +110,7 @@ class UserCutsComponent(Component): self, instance: "Instance", sample: Sample, - ) -> List[Hashable]: + ) -> List[str]: return self.dynamic.sample_predict(instance, sample) @overrides @@ -120,8 +120,8 @@ class UserCutsComponent(Component): @overrides def fit_xy( self, - x: Dict[Hashable, np.ndarray], - y: Dict[Hashable, np.ndarray], + x: Dict[str, np.ndarray], + y: Dict[str, np.ndarray], ) -> None: self.dynamic.fit_xy(x, y) @@ -130,5 +130,5 @@ class UserCutsComponent(Component): self, instance: "Instance", sample: Sample, - ) -> Dict[Hashable, Dict[str, float]]: + ) -> Dict[str, Dict[str, float]]: return self.dynamic.sample_evaluate(instance, sample) diff --git a/miplearn/components/objective.py b/miplearn/components/objective.py index de2852c..c07f766 100644 --- a/miplearn/components/objective.py +++ b/miplearn/components/objective.py @@ -3,7 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging -from typing import List, Dict, Any, TYPE_CHECKING, Tuple, Hashable, Optional +from typing import List, Dict, Any, TYPE_CHECKING, Tuple, Optional import numpy as np from overrides import overrides @@ -53,8 +53,8 @@ class ObjectiveValueComponent(Component): @overrides def fit_xy( self, - x: Dict[Hashable, np.ndarray], - y: Dict[Hashable, np.ndarray], + x: Dict[str, np.ndarray], + y: Dict[str, np.ndarray], ) -> None: for c in ["Upper bound", "Lower bound"]: if c in y: @@ -76,20 +76,20 @@ class ObjectiveValueComponent(Component): self, _: Optional[Instance], sample: Sample, - ) -> Tuple[Dict[Hashable, List[List[float]]], Dict[Hashable, List[List[float]]]]: + ) -> Tuple[Dict[str, List[List[float]]], Dict[str, List[List[float]]]]: lp_instance_features = sample.get("lp_instance_features") if lp_instance_features is None: lp_instance_features = sample.get("instance_features_user") assert lp_instance_features is not None # Features - x: Dict[Hashable, List[List[float]]] = { + x: Dict[str, List[List[float]]] = { "Upper bound": [lp_instance_features], "Lower bound": [lp_instance_features], } # Labels - y: Dict[Hashable, List[List[float]]] = {} + y: Dict[str, List[List[float]]] = {} mip_lower_bound = sample.get("mip_lower_bound") mip_upper_bound = sample.get("mip_upper_bound") if mip_lower_bound is not None: @@ -104,7 +104,7 @@ class ObjectiveValueComponent(Component): self, instance: Instance, sample: Sample, - ) -> Dict[Hashable, Dict[str, float]]: + ) -> Dict[str, Dict[str, float]]: def compare(y_pred: float, y_actual: float) -> Dict[str, float]: err = np.round(abs(y_pred - y_actual), 8) return { @@ -114,7 +114,7 @@ class ObjectiveValueComponent(Component): "Relative error": err / y_actual, } - result: Dict[Hashable, Dict[str, float]] = {} + result: Dict[str, Dict[str, float]] = {} pred = self.sample_predict(sample) actual_ub = sample.get("mip_upper_bound") actual_lb = sample.get("mip_lower_bound") diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index b9d141f..de3a72d 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -3,15 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging -from typing import ( - Dict, - List, - Hashable, - Any, - TYPE_CHECKING, - Tuple, - Optional, -) +from typing import Dict, List, Any, TYPE_CHECKING, Tuple, Optional import numpy as np from overrides import overrides @@ -55,8 +47,8 @@ class PrimalSolutionComponent(Component): assert isinstance(threshold, Threshold) assert mode in ["exact", "heuristic"] self.mode = mode - self.classifiers: Dict[Hashable, Classifier] = {} - self.thresholds: Dict[Hashable, Threshold] = {} + self.classifiers: Dict[str, Classifier] = {} + self.thresholds: Dict[str, Threshold] = {} self.threshold_prototype = threshold self.classifier_prototype = classifier @@ -128,7 +120,7 @@ class PrimalSolutionComponent(Component): # Convert y_pred into solution solution: Solution = {v: None for v in var_names} - category_offset: Dict[Hashable, int] = {cat: 0 for cat in x.keys()} + category_offset: Dict[str, int] = {cat: 0 for cat in x.keys()} for (i, var_name) in enumerate(var_names): category = var_categories[i] if category not in category_offset: @@ -194,7 +186,7 @@ class PrimalSolutionComponent(Component): self, _: Optional[Instance], sample: Sample, - ) -> Dict[Hashable, Dict[str, float]]: + ) -> Dict[str, Dict[str, float]]: mip_var_values = sample.get("mip_var_values") var_names = sample.get("var_names") assert mip_var_values is not None @@ -221,13 +213,13 @@ class PrimalSolutionComponent(Component): pred_one_negative = vars_all - pred_one_positive pred_zero_negative = vars_all - pred_zero_positive return { - 0: classifier_evaluation_dict( + "0": classifier_evaluation_dict( tp=len(pred_zero_positive & vars_zero), tn=len(pred_zero_negative & vars_one), fp=len(pred_zero_positive & vars_one), fn=len(pred_zero_negative & vars_zero), ), - 1: classifier_evaluation_dict( + "1": classifier_evaluation_dict( tp=len(pred_one_positive & vars_one), tn=len(pred_one_negative & vars_zero), fp=len(pred_one_positive & vars_zero), @@ -238,8 +230,8 @@ class PrimalSolutionComponent(Component): @overrides def fit_xy( self, - x: Dict[Hashable, np.ndarray], - y: Dict[Hashable, np.ndarray], + x: Dict[str, np.ndarray], + y: Dict[str, np.ndarray], ) -> None: for category in x.keys(): clf = self.classifier_prototype.clone() diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 2cad2a3..12b127a 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -3,7 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging -from typing import Dict, Tuple, List, Hashable, Any, TYPE_CHECKING, Set, Optional +from typing import Dict, Tuple, List, Any, TYPE_CHECKING, Set, Optional import numpy as np from overrides import overrides @@ -44,11 +44,11 @@ class StaticLazyConstraintsComponent(Component): assert isinstance(classifier, Classifier) self.classifier_prototype: Classifier = classifier self.threshold_prototype: Threshold = threshold - self.classifiers: Dict[Hashable, Classifier] = {} - self.thresholds: Dict[Hashable, Threshold] = {} + self.classifiers: Dict[str, Classifier] = {} + self.thresholds: Dict[str, Threshold] = {} self.pool: Constraints = Constraints() self.violation_tolerance: float = violation_tolerance - self.enforced_cids: Set[Hashable] = set() + self.enforced_cids: Set[str] = set() self.n_restored: int = 0 self.n_iterations: int = 0 @@ -105,8 +105,8 @@ class StaticLazyConstraintsComponent(Component): @overrides def fit_xy( self, - x: Dict[Hashable, np.ndarray], - y: Dict[Hashable, np.ndarray], + x: Dict[str, np.ndarray], + y: Dict[str, np.ndarray], ) -> None: for c in y.keys(): assert c in x @@ -136,9 +136,9 @@ class StaticLazyConstraintsComponent(Component): ) -> None: self._check_and_add(solver) - def sample_predict(self, sample: Sample) -> List[Hashable]: + def sample_predict(self, sample: Sample) -> List[str]: x, y, cids = self._sample_xy_with_cids(sample) - enforced_cids: List[Hashable] = [] + enforced_cids: List[str] = [] for category in x.keys(): if category not in self.classifiers: continue @@ -156,7 +156,7 @@ class StaticLazyConstraintsComponent(Component): self, _: Optional[Instance], sample: Sample, - ) -> Tuple[Dict[Hashable, List[List[float]]], Dict[Hashable, List[List[float]]]]: + ) -> Tuple[Dict[str, List[List[float]]], Dict[str, List[List[float]]]]: x, y, __ = self._sample_xy_with_cids(sample) return x, y @@ -197,13 +197,13 @@ class StaticLazyConstraintsComponent(Component): def _sample_xy_with_cids( self, sample: Sample ) -> Tuple[ - Dict[Hashable, List[List[float]]], - Dict[Hashable, List[List[float]]], - Dict[Hashable, List[str]], + Dict[str, List[List[float]]], + Dict[str, List[List[float]]], + Dict[str, List[str]], ]: - x: Dict[Hashable, List[List[float]]] = {} - y: Dict[Hashable, List[List[float]]] = {} - cids: Dict[Hashable, List[str]] = {} + x: Dict[str, List[List[float]]] = {} + y: Dict[str, List[List[float]]] = {} + cids: Dict[str, List[str]] = {} instance_features = sample.get("instance_features_user") constr_features = sample.get("lp_constr_features") constr_names = sample.get("constr_names") diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index a7f2b11..d950d1a 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -5,7 +5,7 @@ import collections import numbers from math import log, isfinite -from typing import TYPE_CHECKING, Dict, Optional, List, Hashable, Any +from typing import TYPE_CHECKING, Dict, Optional, List, Any import numpy as np @@ -142,7 +142,7 @@ class FeaturesExtractor: instance: "Instance", sample: Sample, ) -> None: - categories: List[Optional[Hashable]] = [] + categories: List[Optional[str]] = [] user_features: List[Optional[List[float]]] = [] var_features_dict = instance.get_variable_features() var_categories_dict = instance.get_variable_categories() @@ -153,9 +153,9 @@ class FeaturesExtractor: user_features.append(None) categories.append(None) continue - category: Hashable = var_categories_dict[var_name] - assert isinstance(category, collections.Hashable), ( - f"Variable category must be be hashable. " + category: str = var_categories_dict[var_name] + assert isinstance(category, str), ( + f"Variable category must be a string. " f"Found {type(category).__name__} instead for var={var_name}." ) categories.append(category) @@ -187,7 +187,7 @@ class FeaturesExtractor: ) -> None: has_static_lazy = instance.has_static_lazy_constraints() user_features: List[Optional[List[float]]] = [] - categories: List[Optional[Hashable]] = [] + categories: List[Optional[str]] = [] lazy: List[bool] = [] constr_categories_dict = instance.get_constraint_categories() constr_features_dict = instance.get_constraint_features() @@ -195,15 +195,15 @@ class FeaturesExtractor: assert constr_names is not None for (cidx, cname) in enumerate(constr_names): - category: Optional[Hashable] = cname + category: Optional[str] = cname if cname in constr_categories_dict: category = constr_categories_dict[cname] if category is None: user_features.append(None) categories.append(None) continue - assert isinstance(category, collections.Hashable), ( - f"Constraint category must be hashable. " + assert isinstance(category, str), ( + f"Constraint category must be a string. " f"Found {type(category).__name__} instead for cname={cname}.", ) categories.append(category) diff --git a/miplearn/instance/base.py b/miplearn/instance/base.py index 17817cf..5544cf0 100644 --- a/miplearn/instance/base.py +++ b/miplearn/instance/base.py @@ -4,7 +4,7 @@ import logging from abc import ABC, abstractmethod -from typing import Any, List, Hashable, TYPE_CHECKING, Dict +from typing import Any, List, TYPE_CHECKING, Dict from miplearn.features.sample import Sample @@ -83,7 +83,7 @@ class Instance(ABC): """ return {} - def get_variable_categories(self) -> Dict[str, Hashable]: + def get_variable_categories(self) -> Dict[str, str]: """ Returns a dictionary mapping the name of each variable to its category. @@ -91,7 +91,6 @@ class Instance(ABC): internal ML model to predict the values of both variables. If a variable is not listed in the dictionary, ML models will ignore the variable. - A category can be any hashable type, such as strings, numbers or tuples. By default, returns {}. """ return {} @@ -99,7 +98,7 @@ class Instance(ABC): def get_constraint_features(self) -> Dict[str, List[float]]: return {} - def get_constraint_categories(self) -> Dict[str, Hashable]: + def get_constraint_categories(self) -> Dict[str, str]: return {} def has_static_lazy_constraints(self) -> bool: @@ -115,7 +114,7 @@ class Instance(ABC): self, solver: "InternalSolver", model: Any, - ) -> List[Hashable]: + ) -> List[str]: """ Returns lazy constraint violations found for the current solution. @@ -125,10 +124,10 @@ class Instance(ABC): resolve the problem. The process repeats until no further lazy constraint violations are found. - Each "violation" is simply a string, a tuple or any other hashable type which - allows the instance to identify unambiguously which lazy constraint should be - generated. In the Traveling Salesman Problem, for example, a subtour - violation could be a frozen set containing the cities in the subtour. + Each "violation" is simply a string which allows the instance to identify + unambiguously which lazy constraint should be generated. In the Traveling + Salesman Problem, for example, a subtour violation could be a string + containing the cities in the subtour. The current solution can be queried with `solver.get_solution()`. If the solver is configured to use lazy callbacks, this solution may be non-integer. @@ -141,7 +140,7 @@ class Instance(ABC): self, solver: "InternalSolver", model: Any, - violation: Hashable, + violation: str, ) -> None: """ Adds constraints to the model to ensure that the given violation is fixed. @@ -167,14 +166,14 @@ class Instance(ABC): def has_user_cuts(self) -> bool: return False - def find_violated_user_cuts(self, model: Any) -> List[Hashable]: + def find_violated_user_cuts(self, model: Any) -> List[str]: return [] def enforce_user_cut( self, solver: "InternalSolver", model: Any, - violation: Hashable, + violation: str, ) -> Any: return None diff --git a/miplearn/instance/picklegz.py b/miplearn/instance/picklegz.py index 80ebcc6..a73b176 100644 --- a/miplearn/instance/picklegz.py +++ b/miplearn/instance/picklegz.py @@ -6,7 +6,7 @@ import gc import gzip import os import pickle -from typing import Optional, Any, List, Hashable, cast, IO, TYPE_CHECKING, Dict +from typing import Optional, Any, List, cast, IO, TYPE_CHECKING, Dict from overrides import overrides @@ -52,7 +52,7 @@ class PickleGzInstance(Instance): return self.instance.get_variable_features() @overrides - def get_variable_categories(self) -> Dict[str, Hashable]: + def get_variable_categories(self) -> Dict[str, str]: assert self.instance is not None return self.instance.get_variable_categories() @@ -62,7 +62,7 @@ class PickleGzInstance(Instance): return self.instance.get_constraint_features() @overrides - def get_constraint_categories(self) -> Dict[str, Hashable]: + def get_constraint_categories(self) -> Dict[str, str]: assert self.instance is not None return self.instance.get_constraint_categories() @@ -86,7 +86,7 @@ class PickleGzInstance(Instance): self, solver: "InternalSolver", model: Any, - ) -> List[Hashable]: + ) -> List[str]: assert self.instance is not None return self.instance.find_violated_lazy_constraints(solver, model) @@ -95,13 +95,13 @@ class PickleGzInstance(Instance): self, solver: "InternalSolver", model: Any, - violation: Hashable, + violation: str, ) -> None: assert self.instance is not None self.instance.enforce_lazy_constraint(solver, model, violation) @overrides - def find_violated_user_cuts(self, model: Any) -> List[Hashable]: + def find_violated_user_cuts(self, model: Any) -> List[str]: assert self.instance is not None return self.instance.find_violated_user_cuts(model) @@ -110,7 +110,7 @@ class PickleGzInstance(Instance): self, solver: "InternalSolver", model: Any, - violation: Hashable, + violation: str, ) -> None: assert self.instance is not None self.instance.enforce_user_cut(solver, model, violation) diff --git a/miplearn/problems/knapsack.py b/miplearn/problems/knapsack.py index 7dfddba..83df03f 100644 --- a/miplearn/problems/knapsack.py +++ b/miplearn/problems/knapsack.py @@ -1,7 +1,8 @@ # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from typing import List, Dict, Optional, Hashable, Any + +from typing import List, Dict, Optional import numpy as np import pyomo.environ as pe @@ -10,7 +11,6 @@ from scipy.stats import uniform, randint, rv_discrete from scipy.stats.distributions import rv_frozen from miplearn.instance.base import Instance -from miplearn.types import VariableName, Category class ChallengeA: diff --git a/miplearn/problems/stab.py b/miplearn/problems/stab.py index 423aebc..db5bff8 100644 --- a/miplearn/problems/stab.py +++ b/miplearn/problems/stab.py @@ -1,7 +1,7 @@ # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from typing import List, Dict, Hashable +from typing import List, Dict import networkx as nx import numpy as np @@ -12,7 +12,6 @@ from scipy.stats import uniform, randint from scipy.stats.distributions import rv_frozen from miplearn.instance.base import Instance -from miplearn.types import VariableName, Category class ChallengeA: @@ -85,7 +84,7 @@ class MaxWeightStableSetInstance(Instance): return features @overrides - def get_variable_categories(self) -> Dict[str, Hashable]: + def get_variable_categories(self) -> Dict[str, str]: return {f"x[{v}]": "default" for v in self.nodes} diff --git a/miplearn/problems/tsp.py b/miplearn/problems/tsp.py index 8fa2598..3ed539f 100644 --- a/miplearn/problems/tsp.py +++ b/miplearn/problems/tsp.py @@ -1,7 +1,7 @@ # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from typing import List, Tuple, FrozenSet, Any, Optional, Hashable, Dict +from typing import List, Tuple, FrozenSet, Any, Optional, Dict import networkx as nx import numpy as np @@ -11,10 +11,9 @@ from scipy.spatial.distance import pdist, squareform from scipy.stats import uniform, randint from scipy.stats.distributions import rv_frozen +from miplearn.instance.base import Instance from miplearn.solvers.learning import InternalSolver from miplearn.solvers.pyomo.base import BasePyomoSolver -from miplearn.instance.base import Instance -from miplearn.types import VariableName, Category class ChallengeA: @@ -82,7 +81,7 @@ class TravelingSalesmanInstance(Instance): return model @overrides - def get_variable_categories(self) -> Dict[str, Hashable]: + def get_variable_categories(self) -> Dict[str, str]: return {f"x[{e}]": f"x[{e}]" for e in self.edges} @overrides diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index 52968cc..a187869 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -6,7 +6,7 @@ import re import sys from io import StringIO from random import randint -from typing import List, Any, Dict, Optional, Hashable, Tuple, TYPE_CHECKING +from typing import List, Any, Dict, Optional, Tuple, TYPE_CHECKING from overrides import overrides @@ -672,7 +672,7 @@ class GurobiTestInstanceKnapsack(PyomoTestInstanceKnapsack): self, solver: InternalSolver, model: Any, - violation: Hashable, + violation: str, ) -> None: x0 = model.getVarByName("x[0]") model.cbLazy(x0 <= 0) diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index 46b044c..d0ffb3c 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -6,7 +6,7 @@ import logging import re import sys from io import StringIO -from typing import Any, List, Dict, Optional, Tuple, Hashable +from typing import Any, List, Dict, Optional, Tuple import numpy as np import pyomo @@ -639,5 +639,5 @@ class PyomoTestInstanceKnapsack(Instance): } @overrides - def get_variable_categories(self) -> Dict[str, Hashable]: + def get_variable_categories(self) -> Dict[str, str]: return {f"x[{i}]": "default" for i in range(len(self.weights))} diff --git a/miplearn/types.py b/miplearn/types.py index ca1cfc4..5239c4c 100644 --- a/miplearn/types.py +++ b/miplearn/types.py @@ -2,7 +2,7 @@ # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from typing import Optional, Dict, Callable, Any, Union, TYPE_CHECKING, Hashable +from typing import Optional, Dict, Callable, Any, Union, TYPE_CHECKING from mypy_extensions import TypedDict @@ -10,7 +10,7 @@ if TYPE_CHECKING: # noinspection PyUnresolvedReferences from miplearn.solvers.learning import InternalSolver -Category = Hashable +Category = str IterationCallback = Callable[[], bool] LazyCallback = Callable[[Any, Any], None] SolverParams = Dict[str, Any] diff --git a/tests/components/test_dynamic_user_cuts.py b/tests/components/test_dynamic_user_cuts.py index 34bac63..cb601dc 100644 --- a/tests/components/test_dynamic_user_cuts.py +++ b/tests/components/test_dynamic_user_cuts.py @@ -3,7 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging -from typing import Any, FrozenSet, Hashable, List +from typing import Any, FrozenSet, List import gurobipy as gp import networkx as nx @@ -40,13 +40,13 @@ class GurobiStableSetProblem(Instance): return True @overrides - def find_violated_user_cuts(self, model: Any) -> List[FrozenSet]: + def find_violated_user_cuts(self, model: Any) -> List[str]: assert isinstance(model, gp.Model) vals = model.cbGetNodeRel(model.getVars()) violations = [] for clique in nx.find_cliques(self.graph): if sum(vals[i] for i in clique) > 1: - violations += [frozenset(clique)] + violations.append(",".join([str(i) for i in clique])) return violations @overrides @@ -54,11 +54,11 @@ class GurobiStableSetProblem(Instance): self, solver: InternalSolver, model: Any, - cid: Hashable, + cid: str, ) -> Any: - assert isinstance(cid, FrozenSet) + clique = [int(i) for i in cid.split(",")] x = model.getVars() - model.addConstr(gp.quicksum([x[i] for i in cid]) <= 1) + model.addConstr(gp.quicksum([x[i] for i in clique]) <= 1) @pytest.fixture diff --git a/tests/components/test_objective.py b/tests/components/test_objective.py index 57ef271..82a6a05 100644 --- a/tests/components/test_objective.py +++ b/tests/components/test_objective.py @@ -1,7 +1,7 @@ # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from typing import Hashable, Dict +from typing import Dict from unittest.mock import Mock import numpy as np @@ -44,11 +44,11 @@ def test_sample_xy(sample: Sample) -> None: def test_fit_xy() -> None: - x: Dict[Hashable, np.ndarray] = { + x: Dict[str, np.ndarray] = { "Lower bound": np.array([[0.0, 0.0], [1.0, 2.0]]), "Upper bound": np.array([[0.0, 0.0], [1.0, 2.0]]), } - y: Dict[Hashable, np.ndarray] = { + y: Dict[str, np.ndarray] = { "Lower bound": np.array([[100.0]]), "Upper bound": np.array([[200.0]]), } diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index e4f2661..91ed584 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -121,8 +121,8 @@ def test_evaluate(sample: Sample) -> None: assert_equals( ev, { - 0: classifier_evaluation_dict(tp=0, fp=1, tn=1, fn=2), - 1: classifier_evaluation_dict(tp=1, fp=1, tn=1, fn=1), + "0": classifier_evaluation_dict(tp=0, fp=1, tn=1, fn=2), + "1": classifier_evaluation_dict(tp=1, fp=1, tn=1, fn=1), }, ) diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index 2412eb8..efe0f40 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -1,7 +1,7 @@ # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from typing import Dict, cast, Hashable +from typing import Dict, cast from unittest.mock import Mock, call import numpy as np @@ -175,14 +175,14 @@ def test_sample_predict(sample: Sample) -> None: def test_fit_xy() -> None: x = cast( - Dict[Hashable, np.ndarray], + Dict[str, np.ndarray], { "type-a": np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]]), "type-b": np.array([[1.0, 4.0, 0.0]]), }, ) y = cast( - Dict[Hashable, np.ndarray], + Dict[str, np.ndarray], { "type-a": np.array([[False, True], [False, True], [True, False]]), "type-b": np.array([[False, True]]), From 4224586d1044a9a3678c183e5dbb94664326550d Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 09:00:04 -0500 Subject: [PATCH 16/67] Remove sample.{get,set} --- miplearn/components/dynamic_common.py | 8 +- miplearn/components/dynamic_lazy.py | 2 +- miplearn/components/dynamic_user_cuts.py | 2 +- miplearn/components/objective.py | 12 +-- miplearn/components/primal.py | 20 ++--- miplearn/components/static_lazy.py | 18 ++-- miplearn/features/extractor.py | 81 +++++++++--------- miplearn/features/sample.py | 92 +++++++------------- miplearn/problems/tsp.py | 10 +-- miplearn/solvers/internal.py | 20 ++--- tests/components/test_dynamic_user_cuts.py | 2 +- tests/components/test_static_lazy.py | 4 +- tests/features/test_extractor.py | 98 +++++++++++----------- tests/problems/test_tsp.py | 10 +-- tests/solvers/test_learning_solver.py | 16 ++-- 15 files changed, 184 insertions(+), 211 deletions(-) diff --git a/miplearn/components/dynamic_common.py b/miplearn/components/dynamic_common.py index 24b375d..fae0177 100644 --- a/miplearn/components/dynamic_common.py +++ b/miplearn/components/dynamic_common.py @@ -52,7 +52,7 @@ class DynamicConstraintsComponent(Component): cids: Dict[str, List[str]] = {} constr_categories_dict = instance.get_constraint_categories() constr_features_dict = instance.get_constraint_features() - instance_features = sample.get("instance_features_user") + instance_features = sample.get_vector("instance_features_user") assert instance_features is not None for cid in self.known_cids: # Initialize categories @@ -81,7 +81,7 @@ class DynamicConstraintsComponent(Component): cids[category].append(cid) # Labels - enforced_cids = sample.get(self.attr) + enforced_cids = sample.get_set(self.attr) if enforced_cids is not None: if cid in enforced_cids: y[category] += [[False, True]] @@ -132,7 +132,7 @@ class DynamicConstraintsComponent(Component): @overrides def pre_sample_xy(self, instance: Instance, sample: Sample) -> Any: - return sample.get(self.attr) + return sample.get_set(self.attr) @overrides def fit_xy( @@ -154,7 +154,7 @@ class DynamicConstraintsComponent(Component): instance: Instance, sample: Sample, ) -> Dict[str, Dict[str, float]]: - actual = sample.get(self.attr) + actual = sample.get_set(self.attr) assert actual is not None pred = set(self.sample_predict(instance, sample)) tp: Dict[str, int] = {} diff --git a/miplearn/components/dynamic_lazy.py b/miplearn/components/dynamic_lazy.py index 1ccfc21..a9145ec 100644 --- a/miplearn/components/dynamic_lazy.py +++ b/miplearn/components/dynamic_lazy.py @@ -78,7 +78,7 @@ class DynamicLazyConstraintsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - sample.put("lazy_enforced", set(self.lazy_enforced)) + sample.put_set("lazy_enforced", set(self.lazy_enforced)) @overrides def iteration_cb( diff --git a/miplearn/components/dynamic_user_cuts.py b/miplearn/components/dynamic_user_cuts.py index c468c08..b60cc44 100644 --- a/miplearn/components/dynamic_user_cuts.py +++ b/miplearn/components/dynamic_user_cuts.py @@ -87,7 +87,7 @@ class UserCutsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - sample.put("user_cuts_enforced", set(self.enforced)) + sample.put_set("user_cuts_enforced", set(self.enforced)) stats["UserCuts: Added in callback"] = self.n_added_in_callback if self.n_added_in_callback > 0: logger.info(f"{self.n_added_in_callback} user cuts added in callback") diff --git a/miplearn/components/objective.py b/miplearn/components/objective.py index c07f766..7fc5385 100644 --- a/miplearn/components/objective.py +++ b/miplearn/components/objective.py @@ -77,9 +77,9 @@ class ObjectiveValueComponent(Component): _: Optional[Instance], sample: Sample, ) -> Tuple[Dict[str, List[List[float]]], Dict[str, List[List[float]]]]: - lp_instance_features = sample.get("lp_instance_features") + lp_instance_features = sample.get_vector("lp_instance_features") if lp_instance_features is None: - lp_instance_features = sample.get("instance_features_user") + lp_instance_features = sample.get_vector("instance_features_user") assert lp_instance_features is not None # Features @@ -90,8 +90,8 @@ class ObjectiveValueComponent(Component): # Labels y: Dict[str, List[List[float]]] = {} - mip_lower_bound = sample.get("mip_lower_bound") - mip_upper_bound = sample.get("mip_upper_bound") + mip_lower_bound = sample.get_scalar("mip_lower_bound") + mip_upper_bound = sample.get_scalar("mip_upper_bound") if mip_lower_bound is not None: y["Lower bound"] = [[mip_lower_bound]] if mip_upper_bound is not None: @@ -116,8 +116,8 @@ class ObjectiveValueComponent(Component): result: Dict[str, Dict[str, float]] = {} pred = self.sample_predict(sample) - actual_ub = sample.get("mip_upper_bound") - actual_lb = sample.get("mip_lower_bound") + actual_ub = sample.get_scalar("mip_upper_bound") + actual_lb = sample.get_scalar("mip_lower_bound") if actual_ub is not None: result["Upper bound"] = compare(pred["Upper bound"], actual_ub) if actual_lb is not None: diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index de3a72d..98be5c4 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -95,8 +95,8 @@ class PrimalSolutionComponent(Component): ) def sample_predict(self, sample: Sample) -> Solution: - var_names = sample.get("var_names") - var_categories = sample.get("var_categories") + var_names = sample.get_vector("var_names") + var_categories = sample.get_vector("var_categories") assert var_names is not None assert var_categories is not None @@ -142,13 +142,13 @@ class PrimalSolutionComponent(Component): ) -> Tuple[Dict[Category, List[List[float]]], Dict[Category, List[List[float]]]]: x: Dict = {} y: Dict = {} - instance_features = sample.get("instance_features_user") - mip_var_values = sample.get("mip_var_values") - var_features = sample.get("lp_var_features") - var_names = sample.get("var_names") - var_categories = sample.get("var_categories") + instance_features = sample.get_vector("instance_features_user") + mip_var_values = sample.get_vector("mip_var_values") + var_features = sample.get_vector_list("lp_var_features") + var_names = sample.get_vector("var_names") + var_categories = sample.get_vector("var_categories") if var_features is None: - var_features = sample.get("var_features") + var_features = sample.get_vector_list("var_features") assert instance_features is not None assert var_features is not None assert var_names is not None @@ -187,8 +187,8 @@ class PrimalSolutionComponent(Component): _: Optional[Instance], sample: Sample, ) -> Dict[str, Dict[str, float]]: - mip_var_values = sample.get("mip_var_values") - var_names = sample.get("var_names") + mip_var_values = sample.get_vector("mip_var_values") + var_names = sample.get_vector("var_names") assert mip_var_values is not None assert var_names is not None diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 12b127a..5a38efc 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -61,7 +61,7 @@ class StaticLazyConstraintsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - sample.put("lazy_enforced", self.enforced_cids) + sample.put_set("lazy_enforced", self.enforced_cids) stats["LazyStatic: Restored"] = self.n_restored stats["LazyStatic: Iterations"] = self.n_iterations @@ -75,7 +75,7 @@ class StaticLazyConstraintsComponent(Component): sample: Sample, ) -> None: assert solver.internal_solver is not None - static_lazy_count = sample.get("static_lazy_count") + static_lazy_count = sample.get_scalar("static_lazy_count") assert static_lazy_count is not None logger.info("Predicting violated (static) lazy constraints...") @@ -204,14 +204,14 @@ class StaticLazyConstraintsComponent(Component): x: Dict[str, List[List[float]]] = {} y: Dict[str, List[List[float]]] = {} cids: Dict[str, List[str]] = {} - instance_features = sample.get("instance_features_user") - constr_features = sample.get("lp_constr_features") - constr_names = sample.get("constr_names") - constr_categories = sample.get("constr_categories") - constr_lazy = sample.get("constr_lazy") - lazy_enforced = sample.get("lazy_enforced") + instance_features = sample.get_vector("instance_features_user") + constr_features = sample.get_vector_list("lp_constr_features") + constr_names = sample.get_vector("constr_names") + constr_categories = sample.get_vector("constr_categories") + constr_lazy = sample.get_vector("constr_lazy") + lazy_enforced = sample.get_set("lazy_enforced") if constr_features is None: - constr_features = sample.get("constr_features_user") + constr_features = sample.get_vector_list("constr_features_user") assert instance_features is not None assert constr_features is not None diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index d950d1a..ff3b9dc 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -39,7 +39,7 @@ class FeaturesExtractor: sample.put_vector("var_types", variables.types) sample.put_vector("var_upper_bounds", variables.upper_bounds) sample.put_vector("constr_names", constraints.names) - sample.put("constr_lhs", constraints.lhs) + # sample.put("constr_lhs", constraints.lhs) sample.put_vector("constr_rhs", constraints.rhs) sample.put_vector("constr_senses", constraints.senses) self._extract_user_features_vars(instance, sample) @@ -49,13 +49,12 @@ class FeaturesExtractor: sample.put_vector_list( "var_features", self._combine( - sample, [ - "var_features_AlvLouWeh2017", - "var_features_user", - "var_lower_bounds", - "var_obj_coeffs", - "var_upper_bounds", + sample.get_vector_list("var_features_AlvLouWeh2017"), + sample.get_vector_list("var_features_user"), + sample.get_vector("var_lower_bounds"), + sample.get_vector("var_obj_coeffs"), + sample.get_vector("var_upper_bounds"), ], ), ) @@ -85,45 +84,43 @@ class FeaturesExtractor: sample.put_vector_list( "lp_var_features", self._combine( - sample, [ - "lp_var_features_AlvLouWeh2017", - "lp_var_reduced_costs", - "lp_var_sa_lb_down", - "lp_var_sa_lb_up", - "lp_var_sa_obj_down", - "lp_var_sa_obj_up", - "lp_var_sa_ub_down", - "lp_var_sa_ub_up", - "lp_var_values", - "var_features_user", - "var_lower_bounds", - "var_obj_coeffs", - "var_upper_bounds", + sample.get_vector_list("lp_var_features_AlvLouWeh2017"), + sample.get_vector("lp_var_reduced_costs"), + sample.get_vector("lp_var_sa_lb_down"), + sample.get_vector("lp_var_sa_lb_up"), + sample.get_vector("lp_var_sa_obj_down"), + sample.get_vector("lp_var_sa_obj_up"), + sample.get_vector("lp_var_sa_ub_down"), + sample.get_vector("lp_var_sa_ub_up"), + sample.get_vector("lp_var_values"), + sample.get_vector_list("var_features_user"), + sample.get_vector("var_lower_bounds"), + sample.get_vector("var_obj_coeffs"), + sample.get_vector("var_upper_bounds"), ], ), ) sample.put_vector_list( "lp_constr_features", self._combine( - sample, [ - "constr_features_user", - "lp_constr_dual_values", - "lp_constr_sa_rhs_down", - "lp_constr_sa_rhs_up", - "lp_constr_slacks", + sample.get_vector_list("constr_features_user"), + sample.get_vector("lp_constr_dual_values"), + sample.get_vector("lp_constr_sa_rhs_down"), + sample.get_vector("lp_constr_sa_rhs_up"), + sample.get_vector("lp_constr_slacks"), ], ), ) - instance_features_user = sample.get("instance_features_user") + instance_features_user = sample.get_vector("instance_features_user") assert instance_features_user is not None sample.put_vector( "lp_instance_features", instance_features_user + [ - sample.get("lp_value"), - sample.get("lp_wallclock_time"), + sample.get_scalar("lp_value"), + sample.get_scalar("lp_wallclock_time"), ], ) @@ -146,7 +143,7 @@ class FeaturesExtractor: user_features: List[Optional[List[float]]] = [] var_features_dict = instance.get_variable_features() var_categories_dict = instance.get_variable_categories() - var_names = sample.get("var_names") + var_names = sample.get_vector("var_names") assert var_names is not None for (i, var_name) in enumerate(var_names): if var_name not in var_categories_dict: @@ -177,7 +174,7 @@ class FeaturesExtractor: ) user_features_i = list(user_features_i) user_features.append(user_features_i) - sample.put("var_categories", categories) + sample.put_vector("var_categories", categories) sample.put_vector_list("var_features_user", user_features) def _extract_user_features_constrs( @@ -191,7 +188,7 @@ class FeaturesExtractor: lazy: List[bool] = [] constr_categories_dict = instance.get_constraint_categories() constr_features_dict = instance.get_constraint_features() - constr_names = sample.get("constr_names") + constr_names = sample.get_vector("constr_names") assert constr_names is not None for (cidx, cname) in enumerate(constr_names): @@ -229,7 +226,7 @@ class FeaturesExtractor: lazy.append(False) sample.put_vector_list("constr_features_user", user_features) sample.put_vector("constr_lazy", lazy) - sample.put("constr_categories", categories) + sample.put_vector("constr_categories", categories) def _extract_user_features_instance( self, @@ -248,7 +245,7 @@ class FeaturesExtractor: f"Instance features must be a list of numbers. " f"Found {type(v).__name__} instead." ) - constr_lazy = sample.get("constr_lazy") + constr_lazy = sample.get_vector("constr_lazy") assert constr_lazy is not None sample.put_vector("instance_features_user", user_features) sample.put_scalar("static_lazy_count", sum(constr_lazy)) @@ -260,10 +257,10 @@ class FeaturesExtractor: sample: Sample, prefix: str = "", ) -> None: - obj_coeffs = sample.get("var_obj_coeffs") - obj_sa_down = sample.get("lp_var_sa_obj_down") - obj_sa_up = sample.get("lp_var_sa_obj_up") - values = sample.get(f"lp_var_values") + obj_coeffs = sample.get_vector("var_obj_coeffs") + obj_sa_down = sample.get_vector("lp_var_sa_obj_down") + obj_sa_up = sample.get_vector("lp_var_sa_obj_up") + values = sample.get_vector(f"lp_var_values") assert obj_coeffs is not None pos_obj_coeff_sum = 0.0 @@ -335,12 +332,10 @@ class FeaturesExtractor: def _combine( self, - sample: Sample, - attrs: List[str], + items: List, ) -> List[List[float]]: combined: List[List[float]] = [] - for attr in attrs: - series = sample.get(attr) + for series in items: if series is None: continue if len(combined) == 0: diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 6d4f131..40b15af 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -4,14 +4,22 @@ from abc import ABC, abstractmethod from copy import deepcopy -from typing import Dict, Optional, Any, Union, List, Tuple, cast +from typing import Dict, Optional, Any, Union, List, Tuple, cast, Set import h5py import numpy as np +from h5py import Dataset from overrides import overrides Scalar = Union[None, bool, str, int, float] -Vector = Union[None, List[bool], List[str], List[int], List[float]] +Vector = Union[ + None, + List[bool], + List[str], + List[int], + List[float], + List[Optional[str]], +] VectorList = Union[ List[List[bool]], List[List[str]], @@ -51,39 +59,16 @@ class Sample(ABC): def put_vector_list(self, key: str, value: VectorList) -> None: pass - @abstractmethod - def get(self, key: str) -> Optional[Any]: - pass - - @abstractmethod - def put(self, key: str, value: Any) -> None: - """ - Add a new key/value pair to the sample. If the key already exists, - the previous value is silently replaced. - - Only the following data types are supported: - - str, bool, int, float - - List[str], List[bool], List[int], List[float] - """ - pass + def get_set(self, key: str) -> Set: + v = self.get_vector(key) + if v: + return set(v) + else: + return set() - def _assert_supported(self, value: Any) -> None: - def _is_primitive(v: Any) -> bool: - if isinstance(v, (str, bool, int, float)): - return True - if v is None: - return True - return False - - if _is_primitive(value): - return - if isinstance(value, list): - if _is_primitive(value[0]): - return - if isinstance(value[0], list): - if _is_primitive(value[0][0]): - return - assert False, f"Value has unsupported type: {value}" + def put_set(self, key: str, value: Set) -> None: + v = list(value) + self.put_vector(key, v) def _assert_is_scalar(self, value: Any) -> None: if value is None: @@ -118,42 +103,40 @@ class MemorySample(Sample): @overrides def get_scalar(self, key: str) -> Optional[Any]: - return self.get(key) + return self._get(key) @overrides def get_vector(self, key: str) -> Optional[Any]: - return self.get(key) + return self._get(key) @overrides def get_vector_list(self, key: str) -> Optional[Any]: - return self.get(key) + return self._get(key) @overrides def put_scalar(self, key: str, value: Scalar) -> None: self._assert_is_scalar(value) - self.put(key, value) + self._put(key, value) @overrides def put_vector(self, key: str, value: Vector) -> None: if value is None: return self._assert_is_vector(value) - self.put(key, value) + self._put(key, value) @overrides def put_vector_list(self, key: str, value: VectorList) -> None: self._assert_is_vector_list(value) - self.put(key, value) + self._put(key, value) - @overrides - def get(self, key: str) -> Optional[Any]: + def _get(self, key: str) -> Optional[Any]: if key in self._data: return self._data[key] else: return None - @overrides - def put(self, key: str, value: Any) -> None: + def _put(self, key: str, value: Any) -> None: self._data[key] = value @@ -200,20 +183,18 @@ class Hdf5Sample(Sample): @overrides def put_scalar(self, key: str, value: Any) -> None: self._assert_is_scalar(value) - self.put(key, value) + self._put(key, value) @overrides def put_vector(self, key: str, value: Vector) -> None: if value is None: return self._assert_is_vector(value) - self.put(key, value) + self._put(key, value) @overrides def put_vector_list(self, key: str, value: VectorList) -> None: self._assert_is_vector_list(value) - if key in self.file: - del self.file[key] padded, lens = _pad(value) data = None for v in value: @@ -227,22 +208,13 @@ class Hdf5Sample(Sample): data = np.array(padded) break assert data is not None - ds = self.file.create_dataset(key, data=data) + ds = self._put(key, data) ds.attrs["lengths"] = lens - @overrides - def get(self, key: str) -> Optional[Any]: - ds = self.file[key] - if h5py.check_string_dtype(ds.dtype): - return ds.asstr()[:].tolist() - else: - return ds[:].tolist() - - @overrides - def put(self, key: str, value: Any) -> None: + def _put(self, key: str, value: Any) -> Dataset: if key in self.file: del self.file[key] - self.file.create_dataset(key, data=value) + return self.file.create_dataset(key, data=value) def _pad(veclist: VectorList) -> Tuple[VectorList, List[int]]: diff --git a/miplearn/problems/tsp.py b/miplearn/problems/tsp.py index 3ed539f..66cae5d 100644 --- a/miplearn/problems/tsp.py +++ b/miplearn/problems/tsp.py @@ -89,15 +89,14 @@ class TravelingSalesmanInstance(Instance): self, solver: InternalSolver, model: Any, - ) -> List[FrozenSet]: + ) -> List[str]: selected_edges = [e for e in self.edges if model.x[e].value > 0.5] graph = nx.Graph() graph.add_edges_from(selected_edges) - components = [frozenset(c) for c in list(nx.connected_components(graph))] violations = [] - for c in components: + for c in list(nx.connected_components(graph)): if len(c) < self.n_cities: - violations += [c] + violations.append(",".join(map(str, c))) return violations @overrides @@ -105,9 +104,10 @@ class TravelingSalesmanInstance(Instance): self, solver: InternalSolver, model: Any, - component: FrozenSet, + violation: str, ) -> None: assert isinstance(solver, BasePyomoSolver) + component = [int(v) for v in violation.split(",")] cut_edges = [ e for e in self.edges diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index f2e20d2..82191c6 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -80,16 +80,16 @@ class Constraints: @staticmethod def from_sample(sample: "Sample") -> "Constraints": return Constraints( - basis_status=sample.get("lp_constr_basis_status"), - dual_values=sample.get("lp_constr_dual_values"), - lazy=sample.get("constr_lazy"), - lhs=sample.get("constr_lhs"), - names=sample.get("constr_names"), - rhs=sample.get("constr_rhs"), - sa_rhs_down=sample.get("lp_constr_sa_rhs_down"), - sa_rhs_up=sample.get("lp_constr_sa_rhs_up"), - senses=sample.get("constr_senses"), - slacks=sample.get("lp_constr_slacks"), + basis_status=sample.get_vector("lp_constr_basis_status"), + dual_values=sample.get_vector("lp_constr_dual_values"), + lazy=sample.get_vector("constr_lazy"), + # lhs=sample.get_vector("constr_lhs"), + names=sample.get_vector("constr_names"), + rhs=sample.get_vector("constr_rhs"), + sa_rhs_down=sample.get_vector("lp_constr_sa_rhs_down"), + sa_rhs_up=sample.get_vector("lp_constr_sa_rhs_up"), + senses=sample.get_vector("constr_senses"), + slacks=sample.get_vector("lp_constr_slacks"), ) def __getitem__(self, selected: List[bool]) -> "Constraints": diff --git a/tests/components/test_dynamic_user_cuts.py b/tests/components/test_dynamic_user_cuts.py index cb601dc..cfb76c0 100644 --- a/tests/components/test_dynamic_user_cuts.py +++ b/tests/components/test_dynamic_user_cuts.py @@ -81,7 +81,7 @@ def test_usage( ) -> None: stats_before = solver.solve(stab_instance) sample = stab_instance.get_samples()[0] - user_cuts_enforced = sample.get("user_cuts_enforced") + user_cuts_enforced = sample.get_set("user_cuts_enforced") assert user_cuts_enforced is not None assert len(user_cuts_enforced) > 0 assert stats_before["UserCuts: Added ahead-of-time"] == 0 diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index efe0f40..7dad7c7 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -93,7 +93,7 @@ def test_usage_with_solver(instance: Instance) -> None: stats: LearningSolveStats = {} sample = instance.get_samples()[0] - assert sample.get("lazy_enforced") is not None + assert sample.get_set("lazy_enforced") is not None # LearningSolver calls before_solve_mip component.before_solve_mip( @@ -142,7 +142,7 @@ def test_usage_with_solver(instance: Instance) -> None: ) # Should update training sample - assert sample.get("lazy_enforced") == {"c1", "c2", "c3", "c4"} + assert sample.get_set("lazy_enforced") == {"c1", "c2", "c3", "c4"} # # Should update stats assert stats["LazyStatic: Removed"] == 1 diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 7c192ea..8068816 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -24,21 +24,23 @@ def test_knapsack() -> None: # after-load # ------------------------------------------------------- extractor.extract_after_load_features(instance, solver, sample) - assert_equals(sample.get("var_names"), ["x[0]", "x[1]", "x[2]", "x[3]", "z"]) - assert_equals(sample.get("var_lower_bounds"), [0.0, 0.0, 0.0, 0.0, 0.0]) - assert_equals(sample.get("var_obj_coeffs"), [505.0, 352.0, 458.0, 220.0, 0.0]) - assert_equals(sample.get("var_types"), ["B", "B", "B", "B", "C"]) - assert_equals(sample.get("var_upper_bounds"), [1.0, 1.0, 1.0, 1.0, 67.0]) + assert_equals(sample.get_vector("var_names"), ["x[0]", "x[1]", "x[2]", "x[3]", "z"]) + assert_equals(sample.get_vector("var_lower_bounds"), [0.0, 0.0, 0.0, 0.0, 0.0]) assert_equals( - sample.get("var_categories"), + sample.get_vector("var_obj_coeffs"), [505.0, 352.0, 458.0, 220.0, 0.0] + ) + assert_equals(sample.get_vector("var_types"), ["B", "B", "B", "B", "C"]) + assert_equals(sample.get_vector("var_upper_bounds"), [1.0, 1.0, 1.0, 1.0, 67.0]) + assert_equals( + sample.get_vector("var_categories"), ["default", "default", "default", "default", None], ) assert_equals( - sample.get("var_features_user"), + sample.get_vector_list("var_features_user"), [[23.0, 505.0], [26.0, 352.0], [20.0, 458.0], [18.0, 220.0], None], ) assert_equals( - sample.get("var_features_AlvLouWeh2017"), + sample.get_vector_list("var_features_AlvLouWeh2017"), [ [1.0, 0.32899, 0.0], [1.0, 0.229316, 0.0], @@ -47,61 +49,63 @@ def test_knapsack() -> None: [0.0, 0.0, 0.0], ], ) - assert sample.get("var_features") is not None - assert_equals(sample.get("constr_names"), ["eq_capacity"]) - assert_equals( - sample.get("constr_lhs"), - [ - [ - ("x[0]", 23.0), - ("x[1]", 26.0), - ("x[2]", 20.0), - ("x[3]", 18.0), - ("z", -1.0), - ], - ], - ) - assert_equals(sample.get("constr_rhs"), [0.0]) - assert_equals(sample.get("constr_senses"), ["="]) - assert_equals(sample.get("constr_features_user"), [None]) - assert_equals(sample.get("constr_categories"), ["eq_capacity"]) - assert_equals(sample.get("constr_lazy"), [False]) - assert_equals(sample.get("instance_features_user"), [67.0, 21.75]) - assert_equals(sample.get("static_lazy_count"), 0) + assert sample.get_vector_list("var_features") is not None + assert_equals(sample.get_vector("constr_names"), ["eq_capacity"]) + # assert_equals( + # sample.get_vector("constr_lhs"), + # [ + # [ + # ("x[0]", 23.0), + # ("x[1]", 26.0), + # ("x[2]", 20.0), + # ("x[3]", 18.0), + # ("z", -1.0), + # ], + # ], + # ) + assert_equals(sample.get_vector("constr_rhs"), [0.0]) + assert_equals(sample.get_vector("constr_senses"), ["="]) + assert_equals(sample.get_vector("constr_features_user"), [None]) + assert_equals(sample.get_vector("constr_categories"), ["eq_capacity"]) + assert_equals(sample.get_vector("constr_lazy"), [False]) + assert_equals(sample.get_vector("instance_features_user"), [67.0, 21.75]) + assert_equals(sample.get_scalar("static_lazy_count"), 0) # after-lp # ------------------------------------------------------- solver.solve_lp() extractor.extract_after_lp_features(solver, sample) assert_equals( - sample.get("lp_var_basis_status"), + sample.get_vector("lp_var_basis_status"), ["U", "B", "U", "L", "U"], ) assert_equals( - sample.get("lp_var_reduced_costs"), + sample.get_vector("lp_var_reduced_costs"), [193.615385, 0.0, 187.230769, -23.692308, 13.538462], ) assert_equals( - sample.get("lp_var_sa_lb_down"), + sample.get_vector("lp_var_sa_lb_down"), [-inf, -inf, -inf, -0.111111, -inf], ) assert_equals( - sample.get("lp_var_sa_lb_up"), + sample.get_vector("lp_var_sa_lb_up"), [1.0, 0.923077, 1.0, 1.0, 67.0], ) assert_equals( - sample.get("lp_var_sa_obj_down"), + sample.get_vector("lp_var_sa_obj_down"), [311.384615, 317.777778, 270.769231, -inf, -13.538462], ) assert_equals( - sample.get("lp_var_sa_obj_up"), + sample.get_vector("lp_var_sa_obj_up"), [inf, 570.869565, inf, 243.692308, inf], ) - assert_equals(sample.get("lp_var_sa_ub_down"), [0.913043, 0.923077, 0.9, 0.0, 43.0]) - assert_equals(sample.get("lp_var_sa_ub_up"), [2.043478, inf, 2.2, inf, 69.0]) - assert_equals(sample.get("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0]) assert_equals( - sample.get("lp_var_features_AlvLouWeh2017"), + sample.get_vector("lp_var_sa_ub_down"), [0.913043, 0.923077, 0.9, 0.0, 43.0] + ) + assert_equals(sample.get_vector("lp_var_sa_ub_up"), [2.043478, inf, 2.2, inf, 69.0]) + assert_equals(sample.get_vector("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0]) + assert_equals( + sample.get_vector_list("lp_var_features_AlvLouWeh2017"), [ [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], @@ -110,19 +114,19 @@ def test_knapsack() -> None: [0.0, 0.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0], ], ) - assert sample.get("lp_var_features") is not None - assert_equals(sample.get("lp_constr_basis_status"), ["N"]) - assert_equals(sample.get("lp_constr_dual_values"), [13.538462]) - assert_equals(sample.get("lp_constr_sa_rhs_down"), [-24.0]) - assert_equals(sample.get("lp_constr_sa_rhs_up"), [2.0]) - assert_equals(sample.get("lp_constr_slacks"), [0.0]) + assert sample.get_vector_list("lp_var_features") is not None + assert_equals(sample.get_vector("lp_constr_basis_status"), ["N"]) + assert_equals(sample.get_vector("lp_constr_dual_values"), [13.538462]) + assert_equals(sample.get_vector("lp_constr_sa_rhs_down"), [-24.0]) + assert_equals(sample.get_vector("lp_constr_sa_rhs_up"), [2.0]) + assert_equals(sample.get_vector("lp_constr_slacks"), [0.0]) # after-mip # ------------------------------------------------------- solver.solve() extractor.extract_after_mip_features(solver, sample) - assert_equals(sample.get("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 61.0]) - assert_equals(sample.get("mip_constr_slacks"), [0.0]) + assert_equals(sample.get_vector("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 61.0]) + assert_equals(sample.get_vector("mip_constr_slacks"), [0.0]) def test_constraint_getindex() -> None: diff --git a/tests/problems/test_tsp.py b/tests/problems/test_tsp.py index 16b9628..894fb57 100644 --- a/tests/problems/test_tsp.py +++ b/tests/problems/test_tsp.py @@ -41,9 +41,9 @@ def test_instance() -> None: solver.solve(instance) assert len(instance.get_samples()) == 1 sample = instance.get_samples()[0] - assert sample.get("mip_var_values") == [1.0, 0.0, 1.0, 1.0, 0.0, 1.0] - assert sample.get("mip_lower_bound") == 4.0 - assert sample.get("mip_upper_bound") == 4.0 + assert sample.get_vector("mip_var_values") == [1.0, 0.0, 1.0, 1.0, 0.0, 1.0] + assert sample.get_scalar("mip_lower_bound") == 4.0 + assert sample.get_scalar("mip_upper_bound") == 4.0 def test_subtour() -> None: @@ -65,10 +65,10 @@ def test_subtour() -> None: samples = instance.get_samples() assert len(samples) == 1 sample = samples[0] - lazy_enforced = sample.get("lazy_enforced") + lazy_enforced = sample.get_set("lazy_enforced") assert lazy_enforced is not None assert len(lazy_enforced) > 0 - assert sample.get("mip_var_values") == [ + assert sample.get_vector("mip_var_values") == [ 1.0, 0.0, 0.0, diff --git a/tests/solvers/test_learning_solver.py b/tests/solvers/test_learning_solver.py index 06b94db..add4507 100644 --- a/tests/solvers/test_learning_solver.py +++ b/tests/solvers/test_learning_solver.py @@ -38,16 +38,18 @@ def test_learning_solver( assert len(instance.get_samples()) > 0 sample = instance.get_samples()[0] - assert sample.get("mip_var_values") == [1.0, 0.0, 1.0, 1.0, 61.0] - assert sample.get("mip_lower_bound") == 1183.0 - assert sample.get("mip_upper_bound") == 1183.0 - mip_log = sample.get("mip_log") + assert sample.get_vector("mip_var_values") == [1.0, 0.0, 1.0, 1.0, 61.0] + assert sample.get_scalar("mip_lower_bound") == 1183.0 + assert sample.get_scalar("mip_upper_bound") == 1183.0 + mip_log = sample.get_scalar("mip_log") assert mip_log is not None assert len(mip_log) > 100 - assert_equals(sample.get("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0]) - assert_equals(sample.get("lp_value"), 1287.923077) - lp_log = sample.get("lp_log") + assert_equals( + sample.get_vector("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0] + ) + assert_equals(sample.get_scalar("lp_value"), 1287.923077) + lp_log = sample.get_scalar("lp_log") assert lp_log is not None assert len(lp_log) > 100 From 962707e8b7057d0b5e9b19d35d750c3416652bff Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 09:25:40 -0500 Subject: [PATCH 17/67] Replace push_sample by create_sample --- miplearn/instance/base.py | 6 ++++-- miplearn/instance/picklegz.py | 4 ++-- miplearn/solvers/learning.py | 3 +-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/miplearn/instance/base.py b/miplearn/instance/base.py index 5544cf0..3f0e7b2 100644 --- a/miplearn/instance/base.py +++ b/miplearn/instance/base.py @@ -6,7 +6,7 @@ import logging from abc import ABC, abstractmethod from typing import Any, List, TYPE_CHECKING, Dict -from miplearn.features.sample import Sample +from miplearn.features.sample import Sample, MemorySample logger = logging.getLogger(__name__) @@ -192,5 +192,7 @@ class Instance(ABC): def get_samples(self) -> List[Sample]: return self._samples - def push_sample(self, sample: Sample) -> None: + def create_sample(self) -> Sample: + sample = MemorySample() self._samples.append(sample) + return sample diff --git a/miplearn/instance/picklegz.py b/miplearn/instance/picklegz.py index a73b176..8472a9d 100644 --- a/miplearn/instance/picklegz.py +++ b/miplearn/instance/picklegz.py @@ -137,9 +137,9 @@ class PickleGzInstance(Instance): return self.instance.get_samples() @overrides - def push_sample(self, sample: Sample) -> None: + def create_sample(self) -> Sample: assert self.instance is not None - self.instance.push_sample(sample) + return self.instance.create_sample() def write_pickle_gz(obj: Any, filename: str) -> None: diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index efb10b7..648020a 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -150,8 +150,7 @@ class LearningSolver: # Initialize training sample # ------------------------------------------------------- - sample = MemorySample() - instance.push_sample(sample) + sample = instance.create_sample() # Initialize stats # ------------------------------------------------------- From 284ba15db6840d063325422eb26c7979d69ef646 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 10:01:32 -0500 Subject: [PATCH 18/67] Implement sample.{get,put}_bytes --- miplearn/features/sample.py | 28 ++++++++++++++++++++++++++++ tests/features/test_sample.py | 11 +++++++++++ 2 files changed, 39 insertions(+) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 40b15af..e119500 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -35,6 +35,14 @@ VectorList = Union[ class Sample(ABC): """Abstract dictionary-like class that stores training data.""" + @abstractmethod + def get_bytes(self, key: str) -> Optional[bytes]: + pass + + @abstractmethod + def put_bytes(self, key: str, value: bytes) -> None: + pass + @abstractmethod def get_scalar(self, key: str) -> Optional[Any]: pass @@ -101,6 +109,10 @@ class MemorySample(Sample): data = {} self._data: Dict[str, Any] = data + @overrides + def get_bytes(self, key: str) -> Optional[bytes]: + return self._get(key) + @overrides def get_scalar(self, key: str) -> Optional[Any]: return self._get(key) @@ -113,6 +125,11 @@ class MemorySample(Sample): def get_vector_list(self, key: str) -> Optional[Any]: return self._get(key) + @overrides + def put_bytes(self, key: str, value: bytes) -> None: + assert isinstance(value, bytes) + self._put(key, value) + @overrides def put_scalar(self, key: str, value: Scalar) -> None: self._assert_is_scalar(value) @@ -151,6 +168,12 @@ class Hdf5Sample(Sample): def __init__(self, filename: str) -> None: self.file = h5py.File(filename, "r+") + @overrides + def get_bytes(self, key: str) -> Optional[bytes]: + ds = self.file[key] + assert len(ds.shape) == 1 + return ds[()].tobytes() + @overrides def get_scalar(self, key: str) -> Optional[Any]: ds = self.file[key] @@ -180,6 +203,11 @@ class Hdf5Sample(Sample): padded = ds[:].tolist() return _crop(padded, lens) + @overrides + def put_bytes(self, key: str, value: bytes) -> None: + assert isinstance(value, bytes) + self._put(key, np.frombuffer(value, dtype="uint8")) + @overrides def put_scalar(self, key: str, value: Any) -> None: self._assert_is_scalar(value) diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index 5462210..f18ef1a 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -35,6 +35,17 @@ def _test_sample(sample: Sample) -> None: _assert_roundtrip_vector_list(sample, [[1], None, [2, 2], [3, 3, 3]]) _assert_roundtrip_vector_list(sample, [[1.0], None, [2.0, 2.0], [3.0, 3.0, 3.0]]) + # Bytes + _assert_roundtrip_bytes(sample, b"\x00\x01\x02\x03\x04\x05") + + +def _assert_roundtrip_bytes(sample: Sample, expected: Any) -> None: + sample.put_bytes("key", expected) + actual = sample.get_bytes("key") + assert actual == expected + assert actual is not None + _assert_same_type(actual, expected) + def _assert_roundtrip_scalar(sample: Sample, expected: Any) -> None: sample.put_scalar("key", expected) From 3da8d532a82f97c239d405d00f7624684c2159fe Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 10:37:02 -0500 Subject: [PATCH 19/67] Sample: handle None in vectors --- miplearn/features/sample.py | 7 +++++-- tests/features/test_sample.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index e119500..bf07fc7 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -189,7 +189,9 @@ class Hdf5Sample(Sample): assert len(ds.shape) == 1 print(ds.dtype) if h5py.check_string_dtype(ds.dtype): - return ds.asstr()[:].tolist() + result = ds.asstr()[:].tolist() + result = [r if len(r) > 0 else None for r in result] + return result else: return ds[:].tolist() @@ -218,7 +220,8 @@ class Hdf5Sample(Sample): if value is None: return self._assert_is_vector(value) - self._put(key, value) + modified = [v if v is not None else "" for v in value] + self._put(key, modified) @overrides def put_vector_list(self, key: str, value: VectorList) -> None: diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index f18ef1a..7e4bee7 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -24,7 +24,7 @@ def _test_sample(sample: Sample) -> None: _assert_roundtrip_scalar(sample, 1.0) # Vector - _assert_roundtrip_vector(sample, ["A", "BB", "CCC", "こんにちは"]) + _assert_roundtrip_vector(sample, ["A", "BB", "CCC", "こんにちは", None]) _assert_roundtrip_vector(sample, [True, True, False]) _assert_roundtrip_vector(sample, [1, 2, 3]) _assert_roundtrip_vector(sample, [1.0, 2.0, 3.0]) From a0f8bf15d6eaa922034559c1878c9dab37753b99 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 10:45:11 -0500 Subject: [PATCH 20/67] Handle completely empty veclists --- miplearn/features/sample.py | 6 +++--- tests/features/test_sample.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index bf07fc7..6407275 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -238,7 +238,8 @@ class Hdf5Sample(Sample): else: data = np.array(padded) break - assert data is not None + if data is None: + data = np.array(padded) ds = self._put(key, data) ds.attrs["lengths"] = lens @@ -254,7 +255,7 @@ def _pad(veclist: VectorList) -> Tuple[VectorList, List[int]]: maxlen = max(lens) # Find appropriate constant to pad the vectors - constant: Union[int, float, str, None] = None + constant: Union[int, float, str] = 0 for v in veclist: if v is None or len(v) == 0: continue @@ -266,7 +267,6 @@ def _pad(veclist: VectorList) -> Tuple[VectorList, List[int]]: constant = "" else: assert False, f"Unsupported data type: {v[0]}" - assert constant is not None, "veclist must not be completely empty" # Pad vectors for (i, vi) in enumerate(veclist): diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index 7e4bee7..0b57ef9 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -34,6 +34,7 @@ def _test_sample(sample: Sample) -> None: _assert_roundtrip_vector_list(sample, [[True], [False, False], None]) _assert_roundtrip_vector_list(sample, [[1], None, [2, 2], [3, 3, 3]]) _assert_roundtrip_vector_list(sample, [[1.0], None, [2.0, 2.0], [3.0, 3.0, 3.0]]) + _assert_roundtrip_vector_list(sample, [None, None]) # Bytes _assert_roundtrip_bytes(sample, b"\x00\x01\x02\x03\x04\x05") @@ -68,7 +69,8 @@ def _assert_roundtrip_vector_list(sample: Sample, expected: Any) -> None: actual = sample.get_vector_list("key") assert actual == expected assert actual is not None - _assert_same_type(actual[0][0], expected[0][0]) + if actual[0] is not None: + _assert_same_type(actual[0][0], expected[0][0]) def _assert_same_type(actual: Any, expected: Any) -> None: From 6c98986675789698177a935ea2d2dbb2b8a52db9 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 10:49:30 -0500 Subject: [PATCH 21/67] Hdf5Sample: Return None for non-existing keys --- miplearn/features/sample.py | 8 ++++++++ tests/features/test_sample.py | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 6407275..aa644e6 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -170,12 +170,16 @@ class Hdf5Sample(Sample): @overrides def get_bytes(self, key: str) -> Optional[bytes]: + if key not in self.file: + return None ds = self.file[key] assert len(ds.shape) == 1 return ds[()].tobytes() @overrides def get_scalar(self, key: str) -> Optional[Any]: + if key not in self.file: + return None ds = self.file[key] assert len(ds.shape) == 0 if h5py.check_string_dtype(ds.dtype): @@ -185,6 +189,8 @@ class Hdf5Sample(Sample): @overrides def get_vector(self, key: str) -> Optional[Any]: + if key not in self.file: + return None ds = self.file[key] assert len(ds.shape) == 1 print(ds.dtype) @@ -197,6 +203,8 @@ class Hdf5Sample(Sample): @overrides def get_vector_list(self, key: str) -> Optional[Any]: + if key not in self.file: + return None ds = self.file[key] lens = ds.attrs["lengths"] if h5py.check_string_dtype(ds.dtype): diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index 0b57ef9..41452c2 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -39,6 +39,11 @@ def _test_sample(sample: Sample) -> None: # Bytes _assert_roundtrip_bytes(sample, b"\x00\x01\x02\x03\x04\x05") + assert sample.get_scalar("unknown-key") is None + assert sample.get_vector("unknown-key") is None + assert sample.get_vector_list("unknown-key") is None + assert sample.get_bytes("unknown-key") is None + def _assert_roundtrip_bytes(sample: Sample, expected: Any) -> None: sample.put_bytes("key", expected) From f1dc450cbf0f19f92d8d81eae0b83d0adbab2a0d Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 10:55:19 -0500 Subject: [PATCH 22/67] Do nothing on put_scalar(None) --- miplearn/features/sample.py | 4 ++++ tests/features/test_sample.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index aa644e6..e75f42e 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -132,6 +132,8 @@ class MemorySample(Sample): @overrides def put_scalar(self, key: str, value: Scalar) -> None: + if value is None: + return self._assert_is_scalar(value) self._put(key, value) @@ -220,6 +222,8 @@ class Hdf5Sample(Sample): @overrides def put_scalar(self, key: str, value: Any) -> None: + if value is None: + return self._assert_is_scalar(value) self._put(key, value) diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index 41452c2..e07355f 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -39,11 +39,16 @@ def _test_sample(sample: Sample) -> None: # Bytes _assert_roundtrip_bytes(sample, b"\x00\x01\x02\x03\x04\x05") + # Querying unknown keys should return None assert sample.get_scalar("unknown-key") is None assert sample.get_vector("unknown-key") is None assert sample.get_vector_list("unknown-key") is None assert sample.get_bytes("unknown-key") is None + # Putting None should not modify HDF5 file + sample.put_scalar("key", None) + sample.put_vector("key", None) + def _assert_roundtrip_bytes(sample: Sample, expected: Any) -> None: sample.put_bytes("key", expected) From 15e08f6c367c81e2cb61f78ea528eed87bbbe892 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 11:02:04 -0500 Subject: [PATCH 23/67] Implement FileInstance --- miplearn/instance/file.py | 132 ++++++++++++++++++++++++++++++++++++ tests/instance/test_file.py | 32 +++++++++ 2 files changed, 164 insertions(+) create mode 100644 miplearn/instance/file.py create mode 100644 tests/instance/test_file.py diff --git a/miplearn/instance/file.py b/miplearn/instance/file.py new file mode 100644 index 0000000..f0e5664 --- /dev/null +++ b/miplearn/instance/file.py @@ -0,0 +1,132 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. +import gc +import os +from typing import Any, Optional, List, Dict, TYPE_CHECKING +import pickle + +from overrides import overrides + +from miplearn.features.sample import Hdf5Sample, Sample +from miplearn.instance.base import Instance + +if TYPE_CHECKING: + from miplearn.solvers.learning import InternalSolver + + +class FileInstance(Instance): + def __init__(self, filename: str) -> None: + super().__init__() + assert os.path.exists(filename), f"File not found: {filename}" + self.h5 = Hdf5Sample(filename) + self.instance: Optional[Instance] = None + + # Delegation + # ------------------------------------------------------------------------- + @overrides + def to_model(self) -> Any: + assert self.instance is not None + return self.instance.to_model() + + @overrides + def get_instance_features(self) -> List[float]: + assert self.instance is not None + return self.instance.get_instance_features() + + @overrides + def get_variable_features(self) -> Dict[str, List[float]]: + assert self.instance is not None + return self.instance.get_variable_features() + + @overrides + def get_variable_categories(self) -> Dict[str, str]: + assert self.instance is not None + return self.instance.get_variable_categories() + + @overrides + def get_constraint_features(self) -> Dict[str, List[float]]: + assert self.instance is not None + return self.instance.get_constraint_features() + + @overrides + def get_constraint_categories(self) -> Dict[str, str]: + assert self.instance is not None + return self.instance.get_constraint_categories() + + @overrides + def has_static_lazy_constraints(self) -> bool: + assert self.instance is not None + return self.instance.has_static_lazy_constraints() + + @overrides + def has_dynamic_lazy_constraints(self) -> bool: + assert self.instance is not None + return self.instance.has_dynamic_lazy_constraints() + + @overrides + def is_constraint_lazy(self, cid: str) -> bool: + assert self.instance is not None + return self.instance.is_constraint_lazy(cid) + + @overrides + def find_violated_lazy_constraints( + self, + solver: "InternalSolver", + model: Any, + ) -> List[str]: + assert self.instance is not None + return self.instance.find_violated_lazy_constraints(solver, model) + + @overrides + def enforce_lazy_constraint( + self, + solver: "InternalSolver", + model: Any, + violation: str, + ) -> None: + assert self.instance is not None + self.instance.enforce_lazy_constraint(solver, model, violation) + + @overrides + def find_violated_user_cuts(self, model: Any) -> List[str]: + assert self.instance is not None + return self.instance.find_violated_user_cuts(model) + + @overrides + def enforce_user_cut( + self, + solver: "InternalSolver", + model: Any, + violation: str, + ) -> None: + assert self.instance is not None + self.instance.enforce_user_cut(solver, model, violation) + + # Input & Output + # ------------------------------------------------------------------------- + @overrides + def free(self) -> None: + self.instance = None + gc.collect() + + @overrides + def load(self) -> None: + if self.instance is not None: + return + self.instance = pickle.loads(self.h5.get_bytes("pickled")) + assert isinstance(self.instance, Instance) + + @classmethod + def save(cls, instance: Instance, filename: str) -> None: + h5 = Hdf5Sample(filename) + instance_pkl = pickle.dumps(instance) + h5.put_bytes("pickled", instance_pkl) + + @overrides + def create_sample(self) -> Sample: + return self.h5 + + @overrides + def get_samples(self) -> List[Sample]: + return [self.h5] diff --git a/tests/instance/test_file.py b/tests/instance/test_file.py new file mode 100644 index 0000000..14c7c73 --- /dev/null +++ b/tests/instance/test_file.py @@ -0,0 +1,32 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +import tempfile + +from miplearn.solvers.learning import LearningSolver +from miplearn.solvers.gurobi import GurobiSolver +from miplearn.features.sample import Hdf5Sample +from miplearn.instance.file import FileInstance + + +def test_usage() -> None: + # Create original instance + original = GurobiSolver().build_test_instance_knapsack() + + # Save instance to disk + file = tempfile.NamedTemporaryFile() + FileInstance.save(original, file.name) + sample = Hdf5Sample(file.name) + assert len(sample.get_bytes("pickled")) > 0 + + # Solve instance from disk + solver = LearningSolver(solver=GurobiSolver()) + solver.solve(FileInstance(file.name)) + + # Assert HDF5 contains training data + sample = FileInstance(file.name).get_samples()[0] + assert sample.get_scalar("mip_lower_bound") == 1183.0 + assert sample.get_scalar("mip_upper_bound") == 1183.0 + assert len(sample.get_vector("lp_var_values")) == 5 + assert len(sample.get_vector("mip_var_values")) == 5 From 4f14b99a75e443807c50aad8b1a962b1467cb102 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 11:12:07 -0500 Subject: [PATCH 24/67] Add h5py to setup.py --- setup.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index c0fe2ef..e39f85c 100644 --- a/setup.py +++ b/setup.py @@ -19,20 +19,21 @@ setup( packages=find_namespace_packages(), python_requires=">=3.7", install_requires=[ + "decorator>=4,<5", + "h5py>=3,<4", "matplotlib>=3,<4", + "mypy==0.790", "networkx>=2,<3", "numpy>=1,<1.21", + "overrides>=3,<4", "p_tqdm>=1,<2", "pandas>=1,<2", "pyomo>=5,<6", "pytest>=6,<7", "python-markdown-math>=0.8,<0.9", - "seaborn>=0.11,<0.12", "scikit-learn>=0.24,<0.25", + "seaborn>=0.11,<0.12", "tqdm>=4,<5", - "mypy==0.790", - "decorator>=4,<5", - "overrides>=3,<4", ], extras_require={ "dev": [ From d30c3232e6c4d51886da954b537c69091db7b866 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 11:22:40 -0500 Subject: [PATCH 25/67] FileInstance.save: create file when it does not already exist --- miplearn/features/sample.py | 4 ++-- miplearn/instance/file.py | 2 +- tests/instance/test_file.py | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index e75f42e..5c19172 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -167,8 +167,8 @@ class Hdf5Sample(Sample): are actually accessed, and therefore it is more scalable. """ - def __init__(self, filename: str) -> None: - self.file = h5py.File(filename, "r+") + def __init__(self, filename: str, mode: str = "r+") -> None: + self.file = h5py.File(filename, mode) @overrides def get_bytes(self, key: str) -> Optional[bytes]: diff --git a/miplearn/instance/file.py b/miplearn/instance/file.py index f0e5664..14f9fdf 100644 --- a/miplearn/instance/file.py +++ b/miplearn/instance/file.py @@ -119,7 +119,7 @@ class FileInstance(Instance): @classmethod def save(cls, instance: Instance, filename: str) -> None: - h5 = Hdf5Sample(filename) + h5 = Hdf5Sample(filename, mode="w") instance_pkl = pickle.dumps(instance) h5.put_bytes("pickled", instance_pkl) diff --git a/tests/instance/test_file.py b/tests/instance/test_file.py index 14c7c73..6ff9767 100644 --- a/tests/instance/test_file.py +++ b/tests/instance/test_file.py @@ -15,17 +15,17 @@ def test_usage() -> None: original = GurobiSolver().build_test_instance_knapsack() # Save instance to disk - file = tempfile.NamedTemporaryFile() - FileInstance.save(original, file.name) - sample = Hdf5Sample(file.name) + filename = tempfile.mktemp() + FileInstance.save(original, filename) + sample = Hdf5Sample(filename) assert len(sample.get_bytes("pickled")) > 0 # Solve instance from disk solver = LearningSolver(solver=GurobiSolver()) - solver.solve(FileInstance(file.name)) + solver.solve(FileInstance(filename)) # Assert HDF5 contains training data - sample = FileInstance(file.name).get_samples()[0] + sample = FileInstance(filename).get_samples()[0] assert sample.get_scalar("mip_lower_bound") == 1183.0 assert sample.get_scalar("mip_upper_bound") == 1183.0 assert len(sample.get_vector("lp_var_values")) == 5 From 728a6bc83529f666c6b5bad53aea172d145d3a21 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 11:24:41 -0500 Subject: [PATCH 26/67] Remove debug statement --- miplearn/features/sample.py | 1 - 1 file changed, 1 deletion(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 5c19172..41c7f52 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -195,7 +195,6 @@ class Hdf5Sample(Sample): return None ds = self.file[key] assert len(ds.shape) == 1 - print(ds.dtype) if h5py.check_string_dtype(ds.dtype): result = ds.asstr()[:].tolist() result = [r if len(r) > 0 else None for r in result] From b6880f068cf2022a7f11559024d5e04d653e8962 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 11:47:26 -0500 Subject: [PATCH 27/67] Hdf5Sample: store lengths as dataset instead of attr --- miplearn/features/sample.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 41c7f52..078c847 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -207,7 +207,7 @@ class Hdf5Sample(Sample): if key not in self.file: return None ds = self.file[key] - lens = ds.attrs["lengths"] + lens = self.get_vector(f"{key}_lengths") if h5py.check_string_dtype(ds.dtype): padded = ds.asstr()[:].tolist() else: @@ -238,6 +238,7 @@ class Hdf5Sample(Sample): def put_vector_list(self, key: str, value: VectorList) -> None: self._assert_is_vector_list(value) padded, lens = _pad(value) + self.put_vector(f"{key}_lengths", lens) data = None for v in value: if v is None or len(v) == 0: @@ -251,8 +252,7 @@ class Hdf5Sample(Sample): break if data is None: data = np.array(padded) - ds = self._put(key, data) - ds.attrs["lengths"] = lens + self._put(key, data) def _put(self, key: str, value: Any) -> Dataset: if key in self.file: From 6fd839351cad4b4f6ec97891fa333c637909aaf6 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 27 Jul 2021 11:50:03 -0500 Subject: [PATCH 28/67] GurobiSolver: Fix error messages --- miplearn/solvers/gurobi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index a187869..20982f9 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -579,16 +579,16 @@ class GurobiSolver(InternalSolver): if var_types[i] == "I": assert var_ubs[i] == 1.0, ( "Only binary and continuous variables are currently supported. " - "Integer variable {var.varName} has upper bound {var.ub}." + f"Integer variable {var_names[i]} has upper bound {var_ubs[i]}." ) assert var_lbs[i] == 0.0, ( "Only binary and continuous variables are currently supported. " - "Integer variable {var.varName} has lower bound {var.ub}." + f"Integer variable {var_names[i]} has lower bound {var_ubs[i]}." ) var_types[i] = "B" assert var_types[i] in ["B", "C"], ( "Only binary and continuous variables are currently supported. " - "Variable {var.varName} has type {vtype}." + f"Variable {var_names[i]} has type {var_types[i]}." ) varname_to_var[var_names[i]] = gp_var for (i, gp_constr) in enumerate(gp_constrs): From fc55a077f2404d31b8a38570f85065625ba6fb6c Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 28 Jul 2021 08:21:56 -0500 Subject: [PATCH 29/67] Sample: Allow numpy arrays --- miplearn/features/sample.py | 9 +++++++-- tests/features/test_sample.py | 12 +++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 078c847..5f6f85f 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -19,6 +19,7 @@ Vector = Union[ List[int], List[float], List[Optional[str]], + np.ndarray, ] VectorList = Union[ List[List[bool]], @@ -86,12 +87,16 @@ class Sample(ABC): assert False, f"Scalar expected; found instead: {value}" def _assert_is_vector(self, value: Any) -> None: - assert isinstance(value, list), f"List expected; found instead: {value}" + assert isinstance( + value, (list, np.ndarray) + ), f"List or numpy array expected; found instead: {value}" for v in value: self._assert_is_scalar(v) def _assert_is_vector_list(self, value: Any) -> None: - assert isinstance(value, list), f"List expected; found instead: {value}" + assert isinstance( + value, (list, np.ndarray) + ), f"List or numpy array expected; found instead: {value}" for v in value: if v is None: continue diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index e07355f..3051470 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -3,8 +3,10 @@ # Released under the modified BSD license. See COPYING.md for more details. from tempfile import NamedTemporaryFile from typing import Any +import numpy as np from miplearn.features.sample import MemorySample, Sample, Hdf5Sample, _pad, _crop +from miplearn.solvers.tests import assert_equals def test_memory_sample() -> None: @@ -28,6 +30,7 @@ def _test_sample(sample: Sample) -> None: _assert_roundtrip_vector(sample, [True, True, False]) _assert_roundtrip_vector(sample, [1, 2, 3]) _assert_roundtrip_vector(sample, [1.0, 2.0, 3.0]) + _assert_roundtrip_vector(sample, np.array([1.0, 2.0, 3.0]), check_type=False) # VectorList _assert_roundtrip_vector_list(sample, [["A"], ["BB", "CCC"], None]) @@ -66,12 +69,15 @@ def _assert_roundtrip_scalar(sample: Sample, expected: Any) -> None: _assert_same_type(actual, expected) -def _assert_roundtrip_vector(sample: Sample, expected: Any) -> None: +def _assert_roundtrip_vector( + sample: Sample, expected: Any, check_type: bool = True +) -> None: sample.put_vector("key", expected) actual = sample.get_vector("key") - assert actual == expected + assert_equals(actual, expected) assert actual is not None - _assert_same_type(actual[0], expected[0]) + if check_type: + _assert_same_type(actual[0], expected[0]) def _assert_roundtrip_vector_list(sample: Sample, expected: Any) -> None: From a69cbed7b7f6a3c54c7e1c1bcdb6d00108956652 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 28 Jul 2021 08:57:09 -0500 Subject: [PATCH 30/67] Improve error messages in assertions --- miplearn/features/sample.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 5f6f85f..2c0c8f7 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -84,19 +84,19 @@ class Sample(ABC): return if isinstance(value, (str, bool, int, float)): return - assert False, f"Scalar expected; found instead: {value}" + assert False, f"scalar expected; found instead: {value}" def _assert_is_vector(self, value: Any) -> None: assert isinstance( value, (list, np.ndarray) - ), f"List or numpy array expected; found instead: {value}" + ), f"list or numpy array expected; found instead: {value}" for v in value: self._assert_is_scalar(v) def _assert_is_vector_list(self, value: Any) -> None: assert isinstance( value, (list, np.ndarray) - ), f"List or numpy array expected; found instead: {value}" + ), f"list or numpy array expected; found instead: {value}" for v in value: if v is None: continue @@ -132,7 +132,7 @@ class MemorySample(Sample): @overrides def put_bytes(self, key: str, value: bytes) -> None: - assert isinstance(value, bytes) + assert isinstance(value, bytes), f"bytes expected; found: {value}" self._put(key, value) @overrides @@ -180,7 +180,9 @@ class Hdf5Sample(Sample): if key not in self.file: return None ds = self.file[key] - assert len(ds.shape) == 1 + assert ( + len(ds.shape) == 1 + ), f"1-dimensional array expected; found shape {ds.shape}" return ds[()].tobytes() @overrides @@ -188,7 +190,9 @@ class Hdf5Sample(Sample): if key not in self.file: return None ds = self.file[key] - assert len(ds.shape) == 0 + assert ( + len(ds.shape) == 0 + ), f"0-dimensional array expected; found shape {ds.shape}" if h5py.check_string_dtype(ds.dtype): return ds.asstr()[()] else: @@ -199,7 +203,9 @@ class Hdf5Sample(Sample): if key not in self.file: return None ds = self.file[key] - assert len(ds.shape) == 1 + assert ( + len(ds.shape) == 1 + ), f"1-dimensional array expected; found shape {ds.shape}" if h5py.check_string_dtype(ds.dtype): result = ds.asstr()[:].tolist() result = [r if len(r) > 0 else None for r in result] @@ -221,7 +227,7 @@ class Hdf5Sample(Sample): @overrides def put_bytes(self, key: str, value: bytes) -> None: - assert isinstance(value, bytes) + assert isinstance(value, bytes), f"bytes expected; found: {value}" self._put(key, np.frombuffer(value, dtype="uint8")) @overrides @@ -282,13 +288,13 @@ def _pad(veclist: VectorList) -> Tuple[VectorList, List[int]]: elif isinstance(v[0], str): constant = "" else: - assert False, f"Unsupported data type: {v[0]}" + assert False, f"unsupported data type: {v[0]}" # Pad vectors for (i, vi) in enumerate(veclist): if vi is None: vi = veclist[i] = [] - assert isinstance(vi, list) + assert isinstance(vi, list), f"list expected; found: {vi}" for k in range(len(vi), maxlen): vi.append(constant) From 7d5ec1344ac5b80dcef91bf50d7a6847dd6fdfb7 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 28 Jul 2021 09:06:15 -0500 Subject: [PATCH 31/67] Make Hdf5Sample work with bytearray --- miplearn/features/sample.py | 21 +++++++++++++-------- tests/features/test_sample.py | 12 ++++++++++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 2c0c8f7..5503343 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -11,6 +11,7 @@ import numpy as np from h5py import Dataset from overrides import overrides +Bytes = Union[bytes, bytearray] Scalar = Union[None, bool, str, int, float] Vector = Union[ None, @@ -37,11 +38,11 @@ class Sample(ABC): """Abstract dictionary-like class that stores training data.""" @abstractmethod - def get_bytes(self, key: str) -> Optional[bytes]: + def get_bytes(self, key: str) -> Optional[Bytes]: pass @abstractmethod - def put_bytes(self, key: str, value: bytes) -> None: + def put_bytes(self, key: str, value: Bytes) -> None: pass @abstractmethod @@ -115,7 +116,7 @@ class MemorySample(Sample): self._data: Dict[str, Any] = data @overrides - def get_bytes(self, key: str) -> Optional[bytes]: + def get_bytes(self, key: str) -> Optional[Bytes]: return self._get(key) @overrides @@ -131,8 +132,10 @@ class MemorySample(Sample): return self._get(key) @overrides - def put_bytes(self, key: str, value: bytes) -> None: - assert isinstance(value, bytes), f"bytes expected; found: {value}" + def put_bytes(self, key: str, value: Bytes) -> None: + assert isinstance( + value, (bytes, bytearray) + ), f"bytes expected; found: {value}" # type: ignore self._put(key, value) @overrides @@ -176,7 +179,7 @@ class Hdf5Sample(Sample): self.file = h5py.File(filename, mode) @overrides - def get_bytes(self, key: str) -> Optional[bytes]: + def get_bytes(self, key: str) -> Optional[Bytes]: if key not in self.file: return None ds = self.file[key] @@ -226,8 +229,10 @@ class Hdf5Sample(Sample): return _crop(padded, lens) @overrides - def put_bytes(self, key: str, value: bytes) -> None: - assert isinstance(value, bytes), f"bytes expected; found: {value}" + def put_bytes(self, key: str, value: Bytes) -> None: + assert isinstance( + value, (bytes, bytearray) + ), f"bytes expected; found: {value}" # type: ignore self._put(key, np.frombuffer(value, dtype="uint8")) @overrides diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index 3051470..32b80bd 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -41,6 +41,11 @@ def _test_sample(sample: Sample) -> None: # Bytes _assert_roundtrip_bytes(sample, b"\x00\x01\x02\x03\x04\x05") + _assert_roundtrip_bytes( + sample, + bytearray(b"\x00\x01\x02\x03\x04\x05"), + check_type=False, + ) # Querying unknown keys should return None assert sample.get_scalar("unknown-key") is None @@ -53,12 +58,15 @@ def _test_sample(sample: Sample) -> None: sample.put_vector("key", None) -def _assert_roundtrip_bytes(sample: Sample, expected: Any) -> None: +def _assert_roundtrip_bytes( + sample: Sample, expected: Any, check_type: bool = False +) -> None: sample.put_bytes("key", expected) actual = sample.get_bytes("key") assert actual == expected assert actual is not None - _assert_same_type(actual, expected) + if check_type: + _assert_same_type(actual, expected) def _assert_roundtrip_scalar(sample: Sample, expected: Any) -> None: From 7163472cfc3c35ffc2b54958ef45b15beb499c8b Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 28 Jul 2021 09:33:40 -0500 Subject: [PATCH 32/67] Bump version to 0.2.0.dev11 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e39f85c..c82b8ac 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ with open("README.md", "r") as fh: setup( name="miplearn", - version="0.2.0.dev10", + version="0.2.0.dev11", author="Alinson S. Xavier", author_email="axavier@anl.gov", description="Extensible framework for Learning-Enhanced Mixed-Integer Optimization", From c513515725bef3bd7844cdcf4e1a3480cb57b35c Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 28 Jul 2021 10:14:55 -0500 Subject: [PATCH 33/67] Hdf5Sample: Enable compression --- miplearn/features/sample.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 5503343..917bb2b 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -233,7 +233,7 @@ class Hdf5Sample(Sample): assert isinstance( value, (bytes, bytearray) ), f"bytes expected; found: {value}" # type: ignore - self._put(key, np.frombuffer(value, dtype="uint8")) + self._put(key, np.frombuffer(value, dtype="uint8"), compress=True) @overrides def put_scalar(self, key: str, value: Any) -> None: @@ -248,7 +248,7 @@ class Hdf5Sample(Sample): return self._assert_is_vector(value) modified = [v if v is not None else "" for v in value] - self._put(key, modified) + self._put(key, modified, compress=True) @overrides def put_vector_list(self, key: str, value: VectorList) -> None: @@ -268,12 +268,16 @@ class Hdf5Sample(Sample): break if data is None: data = np.array(padded) - self._put(key, data) + self._put(key, data, compress=True) - def _put(self, key: str, value: Any) -> Dataset: + def _put(self, key: str, value: Any, compress: bool = False) -> Dataset: if key in self.file: del self.file[key] - return self.file.create_dataset(key, data=value) + if compress: + ds = self.file.create_dataset(key, data=value, compression="gzip") + else: + ds = self.file.create_dataset(key, data=value) + return ds def _pad(veclist: VectorList) -> Tuple[VectorList, List[int]]: From 865a4b2f4070653823b589d3e08917a4634df6dc Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 4 Aug 2021 11:34:56 -0500 Subject: [PATCH 34/67] Hdf5Sample: Store string vectors as "S" dtype instead of obj --- miplearn/features/sample.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 917bb2b..0dd6005 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -247,8 +247,16 @@ class Hdf5Sample(Sample): if value is None: return self._assert_is_vector(value) - modified = [v if v is not None else "" for v in value] - self._put(key, modified, compress=True) + + for v in value: + if isinstance(v, str): + value = np.array( + [u if u is not None else b"" for u in value], + dtype="S", + ) + break + + self._put(key, value, compress=True) @overrides def put_vector_list(self, key: str, value: VectorList) -> None: From 10eed9b30618b65795bfc3f36fcfd69a4ca96dbe Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 4 Aug 2021 13:22:12 -0500 Subject: [PATCH 35/67] Don't include intermediary features in sample; rename some keys --- miplearn/components/dynamic_common.py | 2 +- miplearn/components/objective.py | 2 +- miplearn/components/primal.py | 2 +- miplearn/components/static_lazy.py | 4 +-- miplearn/features/extractor.py | 42 +++++++++++++-------------- tests/components/test_dynamic_lazy.py | 6 ++-- tests/components/test_primal.py | 2 +- tests/components/test_static_lazy.py | 2 +- tests/features/test_extractor.py | 28 ++---------------- tests/features/test_sample.py | 2 +- 10 files changed, 33 insertions(+), 59 deletions(-) diff --git a/miplearn/components/dynamic_common.py b/miplearn/components/dynamic_common.py index fae0177..e319a01 100644 --- a/miplearn/components/dynamic_common.py +++ b/miplearn/components/dynamic_common.py @@ -52,7 +52,7 @@ class DynamicConstraintsComponent(Component): cids: Dict[str, List[str]] = {} constr_categories_dict = instance.get_constraint_categories() constr_features_dict = instance.get_constraint_features() - instance_features = sample.get_vector("instance_features_user") + instance_features = sample.get_vector("instance_features") assert instance_features is not None for cid in self.known_cids: # Initialize categories diff --git a/miplearn/components/objective.py b/miplearn/components/objective.py index 7fc5385..707d699 100644 --- a/miplearn/components/objective.py +++ b/miplearn/components/objective.py @@ -79,7 +79,7 @@ class ObjectiveValueComponent(Component): ) -> Tuple[Dict[str, List[List[float]]], Dict[str, List[List[float]]]]: lp_instance_features = sample.get_vector("lp_instance_features") if lp_instance_features is None: - lp_instance_features = sample.get_vector("instance_features_user") + lp_instance_features = sample.get_vector("instance_features") assert lp_instance_features is not None # Features diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index 98be5c4..5b73f8c 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -142,7 +142,7 @@ class PrimalSolutionComponent(Component): ) -> Tuple[Dict[Category, List[List[float]]], Dict[Category, List[List[float]]]]: x: Dict = {} y: Dict = {} - instance_features = sample.get_vector("instance_features_user") + instance_features = sample.get_vector("instance_features") mip_var_values = sample.get_vector("mip_var_values") var_features = sample.get_vector_list("lp_var_features") var_names = sample.get_vector("var_names") diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 5a38efc..7022f04 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -204,14 +204,14 @@ class StaticLazyConstraintsComponent(Component): x: Dict[str, List[List[float]]] = {} y: Dict[str, List[List[float]]] = {} cids: Dict[str, List[str]] = {} - instance_features = sample.get_vector("instance_features_user") + instance_features = sample.get_vector("instance_features") constr_features = sample.get_vector_list("lp_constr_features") constr_names = sample.get_vector("constr_names") constr_categories = sample.get_vector("constr_categories") constr_lazy = sample.get_vector("constr_lazy") lazy_enforced = sample.get_set("lazy_enforced") if constr_features is None: - constr_features = sample.get_vector_list("constr_features_user") + constr_features = sample.get_vector_list("constr_features") assert instance_features is not None assert constr_features is not None diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index ff3b9dc..d466a72 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -5,7 +5,7 @@ import collections import numbers from math import log, isfinite -from typing import TYPE_CHECKING, Dict, Optional, List, Any +from typing import TYPE_CHECKING, Dict, Optional, List, Any, Tuple import numpy as np @@ -42,16 +42,19 @@ class FeaturesExtractor: # sample.put("constr_lhs", constraints.lhs) sample.put_vector("constr_rhs", constraints.rhs) sample.put_vector("constr_senses", constraints.senses) - self._extract_user_features_vars(instance, sample) + vars_features_user, var_categories = self._extract_user_features_vars( + instance, sample + ) + sample.put_vector("var_categories", var_categories) self._extract_user_features_constrs(instance, sample) self._extract_user_features_instance(instance, sample) - self._extract_var_features_AlvLouWeh2017(sample) + alw17 = self._extract_var_features_AlvLouWeh2017(sample) sample.put_vector_list( "var_features", self._combine( [ - sample.get_vector_list("var_features_AlvLouWeh2017"), - sample.get_vector_list("var_features_user"), + alw17, + vars_features_user, sample.get_vector("var_lower_bounds"), sample.get_vector("var_obj_coeffs"), sample.get_vector("var_upper_bounds"), @@ -80,12 +83,12 @@ class FeaturesExtractor: sample.put_vector("lp_constr_sa_rhs_down", constraints.sa_rhs_down) sample.put_vector("lp_constr_sa_rhs_up", constraints.sa_rhs_up) sample.put_vector("lp_constr_slacks", constraints.slacks) - self._extract_var_features_AlvLouWeh2017(sample, prefix="lp_") + alw17 = self._extract_var_features_AlvLouWeh2017(sample) sample.put_vector_list( "lp_var_features", self._combine( [ - sample.get_vector_list("lp_var_features_AlvLouWeh2017"), + alw17, sample.get_vector("lp_var_reduced_costs"), sample.get_vector("lp_var_sa_lb_down"), sample.get_vector("lp_var_sa_lb_up"), @@ -105,7 +108,7 @@ class FeaturesExtractor: "lp_constr_features", self._combine( [ - sample.get_vector_list("constr_features_user"), + sample.get_vector_list("constr_features"), sample.get_vector("lp_constr_dual_values"), sample.get_vector("lp_constr_sa_rhs_down"), sample.get_vector("lp_constr_sa_rhs_up"), @@ -113,11 +116,11 @@ class FeaturesExtractor: ], ), ) - instance_features_user = sample.get_vector("instance_features_user") - assert instance_features_user is not None + instance_features = sample.get_vector("instance_features") + assert instance_features is not None sample.put_vector( "lp_instance_features", - instance_features_user + instance_features + [ sample.get_scalar("lp_value"), sample.get_scalar("lp_wallclock_time"), @@ -138,7 +141,7 @@ class FeaturesExtractor: self, instance: "Instance", sample: Sample, - ) -> None: + ) -> Tuple[List, List]: categories: List[Optional[str]] = [] user_features: List[Optional[List[float]]] = [] var_features_dict = instance.get_variable_features() @@ -174,8 +177,7 @@ class FeaturesExtractor: ) user_features_i = list(user_features_i) user_features.append(user_features_i) - sample.put_vector("var_categories", categories) - sample.put_vector_list("var_features_user", user_features) + return user_features, categories def _extract_user_features_constrs( self, @@ -224,7 +226,7 @@ class FeaturesExtractor: lazy.append(instance.is_constraint_lazy(cname)) else: lazy.append(False) - sample.put_vector_list("constr_features_user", user_features) + sample.put_vector_list("constr_features", user_features) sample.put_vector("constr_lazy", lazy) sample.put_vector("constr_categories", categories) @@ -247,16 +249,12 @@ class FeaturesExtractor: ) constr_lazy = sample.get_vector("constr_lazy") assert constr_lazy is not None - sample.put_vector("instance_features_user", user_features) + sample.put_vector("instance_features", user_features) sample.put_scalar("static_lazy_count", sum(constr_lazy)) # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based # approximation of strong branching. INFORMS Journal on Computing, 29(1), 185-195. - def _extract_var_features_AlvLouWeh2017( - self, - sample: Sample, - prefix: str = "", - ) -> None: + def _extract_var_features_AlvLouWeh2017(self, sample: Sample) -> List: obj_coeffs = sample.get_vector("var_obj_coeffs") obj_sa_down = sample.get_vector("lp_var_sa_obj_down") obj_sa_up = sample.get_vector("lp_var_sa_obj_up") @@ -328,7 +326,7 @@ class FeaturesExtractor: for v in f: assert isfinite(v), f"non-finite elements detected: {f}" features.append(f) - sample.put_vector_list(f"{prefix}var_features_AlvLouWeh2017", features) + return features def _combine( self, diff --git a/tests/components/test_dynamic_lazy.py b/tests/components/test_dynamic_lazy.py index 7e54831..6d469d5 100644 --- a/tests/components/test_dynamic_lazy.py +++ b/tests/components/test_dynamic_lazy.py @@ -25,13 +25,13 @@ def training_instances() -> List[Instance]: MemorySample( { "lazy_enforced": {"c1", "c2"}, - "instance_features_user": [5.0], + "instance_features": [5.0], }, ), MemorySample( { "lazy_enforced": {"c2", "c3"}, - "instance_features_user": [5.0], + "instance_features": [5.0], }, ), ] @@ -56,7 +56,7 @@ def training_instances() -> List[Instance]: MemorySample( { "lazy_enforced": {"c3", "c4"}, - "instance_features_user": [8.0], + "instance_features": [8.0], }, ) ] diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index 91ed584..d58a4e6 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -25,7 +25,7 @@ def sample() -> Sample: "var_names": ["x[0]", "x[1]", "x[2]", "x[3]"], "var_categories": ["default", None, "default", "default"], "mip_var_values": [0.0, 1.0, 1.0, 0.0], - "instance_features_user": [5.0], + "instance_features": [5.0], "var_features": [ [0.0, 0.0], None, diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index 7dad7c7..b0b2664 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -33,7 +33,7 @@ def sample() -> Sample: ], "constr_lazy": [True, True, True, True, False], "constr_names": ["c1", "c2", "c3", "c4", "c5"], - "instance_features_user": [5.0], + "instance_features": [5.0], "lazy_enforced": {"c1", "c2", "c4"}, "lp_constr_features": [ [1.0, 1.0], diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 8068816..d746277 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -35,20 +35,6 @@ def test_knapsack() -> None: sample.get_vector("var_categories"), ["default", "default", "default", "default", None], ) - assert_equals( - sample.get_vector_list("var_features_user"), - [[23.0, 505.0], [26.0, 352.0], [20.0, 458.0], [18.0, 220.0], None], - ) - assert_equals( - sample.get_vector_list("var_features_AlvLouWeh2017"), - [ - [1.0, 0.32899, 0.0], - [1.0, 0.229316, 0.0], - [1.0, 0.298371, 0.0], - [1.0, 0.143322, 0.0], - [0.0, 0.0, 0.0], - ], - ) assert sample.get_vector_list("var_features") is not None assert_equals(sample.get_vector("constr_names"), ["eq_capacity"]) # assert_equals( @@ -65,10 +51,10 @@ def test_knapsack() -> None: # ) assert_equals(sample.get_vector("constr_rhs"), [0.0]) assert_equals(sample.get_vector("constr_senses"), ["="]) - assert_equals(sample.get_vector("constr_features_user"), [None]) + assert_equals(sample.get_vector("constr_features"), [None]) assert_equals(sample.get_vector("constr_categories"), ["eq_capacity"]) assert_equals(sample.get_vector("constr_lazy"), [False]) - assert_equals(sample.get_vector("instance_features_user"), [67.0, 21.75]) + assert_equals(sample.get_vector("instance_features"), [67.0, 21.75]) assert_equals(sample.get_scalar("static_lazy_count"), 0) # after-lp @@ -104,16 +90,6 @@ def test_knapsack() -> None: ) assert_equals(sample.get_vector("lp_var_sa_ub_up"), [2.043478, inf, 2.2, inf, 69.0]) assert_equals(sample.get_vector("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0]) - assert_equals( - sample.get_vector_list("lp_var_features_AlvLouWeh2017"), - [ - [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], - [1.0, 0.298371, 0.0, 0.0, 1.0, 1.0, 5.232342, 46.051702], - [1.0, 0.143322, 0.0, 0.0, 1.0, -1.0, 46.051702, 3.16515], - [0.0, 0.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0], - ], - ) assert sample.get_vector_list("lp_var_features") is not None assert_equals(sample.get_vector("lp_constr_basis_status"), ["N"]) assert_equals(sample.get_vector("lp_constr_dual_values"), [13.538462]) diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index 32b80bd..a60e87b 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -26,7 +26,7 @@ def _test_sample(sample: Sample) -> None: _assert_roundtrip_scalar(sample, 1.0) # Vector - _assert_roundtrip_vector(sample, ["A", "BB", "CCC", "こんにちは", None]) + _assert_roundtrip_vector(sample, ["A", "BB", "CCC", None]) _assert_roundtrip_vector(sample, [True, True, False]) _assert_roundtrip_vector(sample, [1, 2, 3]) _assert_roundtrip_vector(sample, [1.0, 2.0, 3.0]) From ca925119b365104727c2f61651ca93537e2491ca Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 4 Aug 2021 13:35:16 -0500 Subject: [PATCH 36/67] Add static_ prefix to all static features --- miplearn/components/dynamic_common.py | 2 +- miplearn/components/objective.py | 2 +- miplearn/components/primal.py | 14 +++---- miplearn/components/static_lazy.py | 12 +++--- miplearn/features/extractor.py | 59 +++++++++++++-------------- miplearn/solvers/internal.py | 10 ++--- tests/components/test_dynamic_lazy.py | 6 +-- tests/components/test_primal.py | 8 ++-- tests/components/test_static_lazy.py | 10 ++--- tests/features/test_extractor.py | 38 +++++++++-------- 10 files changed, 82 insertions(+), 79 deletions(-) diff --git a/miplearn/components/dynamic_common.py b/miplearn/components/dynamic_common.py index e319a01..ce78dd2 100644 --- a/miplearn/components/dynamic_common.py +++ b/miplearn/components/dynamic_common.py @@ -52,7 +52,7 @@ class DynamicConstraintsComponent(Component): cids: Dict[str, List[str]] = {} constr_categories_dict = instance.get_constraint_categories() constr_features_dict = instance.get_constraint_features() - instance_features = sample.get_vector("instance_features") + instance_features = sample.get_vector("static_instance_features") assert instance_features is not None for cid in self.known_cids: # Initialize categories diff --git a/miplearn/components/objective.py b/miplearn/components/objective.py index 707d699..cc4e30c 100644 --- a/miplearn/components/objective.py +++ b/miplearn/components/objective.py @@ -79,7 +79,7 @@ class ObjectiveValueComponent(Component): ) -> Tuple[Dict[str, List[List[float]]], Dict[str, List[List[float]]]]: lp_instance_features = sample.get_vector("lp_instance_features") if lp_instance_features is None: - lp_instance_features = sample.get_vector("instance_features") + lp_instance_features = sample.get_vector("static_instance_features") assert lp_instance_features is not None # Features diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index 5b73f8c..50c7991 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -95,8 +95,8 @@ class PrimalSolutionComponent(Component): ) def sample_predict(self, sample: Sample) -> Solution: - var_names = sample.get_vector("var_names") - var_categories = sample.get_vector("var_categories") + var_names = sample.get_vector("static_var_names") + var_categories = sample.get_vector("static_var_categories") assert var_names is not None assert var_categories is not None @@ -142,13 +142,13 @@ class PrimalSolutionComponent(Component): ) -> Tuple[Dict[Category, List[List[float]]], Dict[Category, List[List[float]]]]: x: Dict = {} y: Dict = {} - instance_features = sample.get_vector("instance_features") + instance_features = sample.get_vector("static_instance_features") mip_var_values = sample.get_vector("mip_var_values") var_features = sample.get_vector_list("lp_var_features") - var_names = sample.get_vector("var_names") - var_categories = sample.get_vector("var_categories") + var_names = sample.get_vector("static_var_names") + var_categories = sample.get_vector("static_var_categories") if var_features is None: - var_features = sample.get_vector_list("var_features") + var_features = sample.get_vector_list("static_var_features") assert instance_features is not None assert var_features is not None assert var_names is not None @@ -188,7 +188,7 @@ class PrimalSolutionComponent(Component): sample: Sample, ) -> Dict[str, Dict[str, float]]: mip_var_values = sample.get_vector("mip_var_values") - var_names = sample.get_vector("var_names") + var_names = sample.get_vector("static_var_names") assert mip_var_values is not None assert var_names is not None diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 7022f04..99aba1c 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -75,7 +75,7 @@ class StaticLazyConstraintsComponent(Component): sample: Sample, ) -> None: assert solver.internal_solver is not None - static_lazy_count = sample.get_scalar("static_lazy_count") + static_lazy_count = sample.get_scalar("static_constr_lazy_count") assert static_lazy_count is not None logger.info("Predicting violated (static) lazy constraints...") @@ -204,14 +204,14 @@ class StaticLazyConstraintsComponent(Component): x: Dict[str, List[List[float]]] = {} y: Dict[str, List[List[float]]] = {} cids: Dict[str, List[str]] = {} - instance_features = sample.get_vector("instance_features") + instance_features = sample.get_vector("static_instance_features") constr_features = sample.get_vector_list("lp_constr_features") - constr_names = sample.get_vector("constr_names") - constr_categories = sample.get_vector("constr_categories") - constr_lazy = sample.get_vector("constr_lazy") + constr_names = sample.get_vector("static_constr_names") + constr_categories = sample.get_vector("static_constr_categories") + constr_lazy = sample.get_vector("static_constr_lazy") lazy_enforced = sample.get_set("lazy_enforced") if constr_features is None: - constr_features = sample.get_vector_list("constr_features") + constr_features = sample.get_vector_list("static_constr_features") assert instance_features is not None assert constr_features is not None diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index d466a72..74c359d 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -33,31 +33,31 @@ class FeaturesExtractor: ) -> None: variables = solver.get_variables(with_static=True) constraints = solver.get_constraints(with_static=True, with_lhs=self.with_lhs) - sample.put_vector("var_lower_bounds", variables.lower_bounds) - sample.put_vector("var_names", variables.names) - sample.put_vector("var_obj_coeffs", variables.obj_coeffs) - sample.put_vector("var_types", variables.types) - sample.put_vector("var_upper_bounds", variables.upper_bounds) - sample.put_vector("constr_names", constraints.names) - # sample.put("constr_lhs", constraints.lhs) - sample.put_vector("constr_rhs", constraints.rhs) - sample.put_vector("constr_senses", constraints.senses) + sample.put_vector("static_var_lower_bounds", variables.lower_bounds) + sample.put_vector("static_var_names", variables.names) + sample.put_vector("static_var_obj_coeffs", variables.obj_coeffs) + sample.put_vector("static_var_types", variables.types) + sample.put_vector("static_var_upper_bounds", variables.upper_bounds) + sample.put_vector("static_constr_names", constraints.names) + # sample.put("static_constr_lhs", constraints.lhs) + sample.put_vector("static_constr_rhs", constraints.rhs) + sample.put_vector("static_constr_senses", constraints.senses) vars_features_user, var_categories = self._extract_user_features_vars( instance, sample ) - sample.put_vector("var_categories", var_categories) + sample.put_vector("static_var_categories", var_categories) self._extract_user_features_constrs(instance, sample) self._extract_user_features_instance(instance, sample) alw17 = self._extract_var_features_AlvLouWeh2017(sample) sample.put_vector_list( - "var_features", + "static_var_features", self._combine( [ alw17, vars_features_user, - sample.get_vector("var_lower_bounds"), - sample.get_vector("var_obj_coeffs"), - sample.get_vector("var_upper_bounds"), + sample.get_vector("static_var_lower_bounds"), + sample.get_vector("static_var_obj_coeffs"), + sample.get_vector("static_var_upper_bounds"), ], ), ) @@ -97,10 +97,7 @@ class FeaturesExtractor: sample.get_vector("lp_var_sa_ub_down"), sample.get_vector("lp_var_sa_ub_up"), sample.get_vector("lp_var_values"), - sample.get_vector_list("var_features_user"), - sample.get_vector("var_lower_bounds"), - sample.get_vector("var_obj_coeffs"), - sample.get_vector("var_upper_bounds"), + sample.get_vector_list("static_var_features"), ], ), ) @@ -108,7 +105,7 @@ class FeaturesExtractor: "lp_constr_features", self._combine( [ - sample.get_vector_list("constr_features"), + sample.get_vector_list("static_constr_features"), sample.get_vector("lp_constr_dual_values"), sample.get_vector("lp_constr_sa_rhs_down"), sample.get_vector("lp_constr_sa_rhs_up"), @@ -116,11 +113,11 @@ class FeaturesExtractor: ], ), ) - instance_features = sample.get_vector("instance_features") - assert instance_features is not None + static_instance_features = sample.get_vector("static_instance_features") + assert static_instance_features is not None sample.put_vector( "lp_instance_features", - instance_features + static_instance_features + [ sample.get_scalar("lp_value"), sample.get_scalar("lp_wallclock_time"), @@ -146,7 +143,7 @@ class FeaturesExtractor: user_features: List[Optional[List[float]]] = [] var_features_dict = instance.get_variable_features() var_categories_dict = instance.get_variable_categories() - var_names = sample.get_vector("var_names") + var_names = sample.get_vector("static_var_names") assert var_names is not None for (i, var_name) in enumerate(var_names): if var_name not in var_categories_dict: @@ -190,7 +187,7 @@ class FeaturesExtractor: lazy: List[bool] = [] constr_categories_dict = instance.get_constraint_categories() constr_features_dict = instance.get_constraint_features() - constr_names = sample.get_vector("constr_names") + constr_names = sample.get_vector("static_constr_names") assert constr_names is not None for (cidx, cname) in enumerate(constr_names): @@ -226,9 +223,9 @@ class FeaturesExtractor: lazy.append(instance.is_constraint_lazy(cname)) else: lazy.append(False) - sample.put_vector_list("constr_features", user_features) - sample.put_vector("constr_lazy", lazy) - sample.put_vector("constr_categories", categories) + sample.put_vector_list("static_constr_features", user_features) + sample.put_vector("static_constr_lazy", lazy) + sample.put_vector("static_constr_categories", categories) def _extract_user_features_instance( self, @@ -247,15 +244,15 @@ class FeaturesExtractor: f"Instance features must be a list of numbers. " f"Found {type(v).__name__} instead." ) - constr_lazy = sample.get_vector("constr_lazy") + constr_lazy = sample.get_vector("static_constr_lazy") assert constr_lazy is not None - sample.put_vector("instance_features", user_features) - sample.put_scalar("static_lazy_count", sum(constr_lazy)) + sample.put_vector("static_instance_features", user_features) + sample.put_scalar("static_constr_lazy_count", sum(constr_lazy)) # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based # approximation of strong branching. INFORMS Journal on Computing, 29(1), 185-195. def _extract_var_features_AlvLouWeh2017(self, sample: Sample) -> List: - obj_coeffs = sample.get_vector("var_obj_coeffs") + obj_coeffs = sample.get_vector("static_var_obj_coeffs") obj_sa_down = sample.get_vector("lp_var_sa_obj_down") obj_sa_up = sample.get_vector("lp_var_sa_obj_up") values = sample.get_vector(f"lp_var_values") diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index 82191c6..c462797 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -82,13 +82,13 @@ class Constraints: return Constraints( basis_status=sample.get_vector("lp_constr_basis_status"), dual_values=sample.get_vector("lp_constr_dual_values"), - lazy=sample.get_vector("constr_lazy"), - # lhs=sample.get_vector("constr_lhs"), - names=sample.get_vector("constr_names"), - rhs=sample.get_vector("constr_rhs"), + lazy=sample.get_vector("static_constr_lazy"), + # lhs=sample.get_vector("static_constr_lhs"), + names=sample.get_vector("static_constr_names"), + rhs=sample.get_vector("static_constr_rhs"), sa_rhs_down=sample.get_vector("lp_constr_sa_rhs_down"), sa_rhs_up=sample.get_vector("lp_constr_sa_rhs_up"), - senses=sample.get_vector("constr_senses"), + senses=sample.get_vector("static_constr_senses"), slacks=sample.get_vector("lp_constr_slacks"), ) diff --git a/tests/components/test_dynamic_lazy.py b/tests/components/test_dynamic_lazy.py index 6d469d5..037b904 100644 --- a/tests/components/test_dynamic_lazy.py +++ b/tests/components/test_dynamic_lazy.py @@ -25,13 +25,13 @@ def training_instances() -> List[Instance]: MemorySample( { "lazy_enforced": {"c1", "c2"}, - "instance_features": [5.0], + "static_instance_features": [5.0], }, ), MemorySample( { "lazy_enforced": {"c2", "c3"}, - "instance_features": [5.0], + "static_instance_features": [5.0], }, ), ] @@ -56,7 +56,7 @@ def training_instances() -> List[Instance]: MemorySample( { "lazy_enforced": {"c3", "c4"}, - "instance_features": [8.0], + "static_instance_features": [8.0], }, ) ] diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index d58a4e6..a5958ac 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -22,11 +22,11 @@ from miplearn.solvers.tests import assert_equals def sample() -> Sample: sample = MemorySample( { - "var_names": ["x[0]", "x[1]", "x[2]", "x[3]"], - "var_categories": ["default", None, "default", "default"], + "static_var_names": ["x[0]", "x[1]", "x[2]", "x[3]"], + "static_var_categories": ["default", None, "default", "default"], "mip_var_values": [0.0, 1.0, 1.0, 0.0], - "instance_features": [5.0], - "var_features": [ + "static_instance_features": [5.0], + "static_var_features": [ [0.0, 0.0], None, [1.0, 0.0], diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index b0b2664..6ef4ec9 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -24,16 +24,16 @@ from miplearn.types import ( def sample() -> Sample: sample = MemorySample( { - "constr_categories": [ + "static_constr_categories": [ "type-a", "type-a", "type-a", "type-b", "type-b", ], - "constr_lazy": [True, True, True, True, False], - "constr_names": ["c1", "c2", "c3", "c4", "c5"], - "instance_features": [5.0], + "static_constr_lazy": [True, True, True, True, False], + "static_constr_names": ["c1", "c2", "c3", "c4", "c5"], + "static_instance_features": [5.0], "lazy_enforced": {"c1", "c2", "c4"}, "lp_constr_features": [ [1.0, 1.0], @@ -42,7 +42,7 @@ def sample() -> Sample: [1.0, 4.0, 0.0], None, ], - "static_lazy_count": 4, + "static_constr_lazy_count": 4, }, ) return sample diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index d746277..1053b4b 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -24,21 +24,27 @@ def test_knapsack() -> None: # after-load # ------------------------------------------------------- extractor.extract_after_load_features(instance, solver, sample) - assert_equals(sample.get_vector("var_names"), ["x[0]", "x[1]", "x[2]", "x[3]", "z"]) - assert_equals(sample.get_vector("var_lower_bounds"), [0.0, 0.0, 0.0, 0.0, 0.0]) assert_equals( - sample.get_vector("var_obj_coeffs"), [505.0, 352.0, 458.0, 220.0, 0.0] + sample.get_vector("static_var_names"), ["x[0]", "x[1]", "x[2]", "x[3]", "z"] ) - assert_equals(sample.get_vector("var_types"), ["B", "B", "B", "B", "C"]) - assert_equals(sample.get_vector("var_upper_bounds"), [1.0, 1.0, 1.0, 1.0, 67.0]) assert_equals( - sample.get_vector("var_categories"), + sample.get_vector("static_var_lower_bounds"), [0.0, 0.0, 0.0, 0.0, 0.0] + ) + assert_equals( + sample.get_vector("static_var_obj_coeffs"), [505.0, 352.0, 458.0, 220.0, 0.0] + ) + assert_equals(sample.get_vector("static_var_types"), ["B", "B", "B", "B", "C"]) + assert_equals( + sample.get_vector("static_var_upper_bounds"), [1.0, 1.0, 1.0, 1.0, 67.0] + ) + assert_equals( + sample.get_vector("static_var_categories"), ["default", "default", "default", "default", None], ) - assert sample.get_vector_list("var_features") is not None - assert_equals(sample.get_vector("constr_names"), ["eq_capacity"]) + assert sample.get_vector_list("static_var_features") is not None + assert_equals(sample.get_vector("static_constr_names"), ["eq_capacity"]) # assert_equals( - # sample.get_vector("constr_lhs"), + # sample.get_vector("static_constr_lhs"), # [ # [ # ("x[0]", 23.0), @@ -49,13 +55,13 @@ def test_knapsack() -> None: # ], # ], # ) - assert_equals(sample.get_vector("constr_rhs"), [0.0]) - assert_equals(sample.get_vector("constr_senses"), ["="]) - assert_equals(sample.get_vector("constr_features"), [None]) - assert_equals(sample.get_vector("constr_categories"), ["eq_capacity"]) - assert_equals(sample.get_vector("constr_lazy"), [False]) - assert_equals(sample.get_vector("instance_features"), [67.0, 21.75]) - assert_equals(sample.get_scalar("static_lazy_count"), 0) + assert_equals(sample.get_vector("static_constr_rhs"), [0.0]) + assert_equals(sample.get_vector("static_constr_senses"), ["="]) + assert_equals(sample.get_vector("static_constr_features"), [None]) + assert_equals(sample.get_vector("static_constr_categories"), ["eq_capacity"]) + assert_equals(sample.get_vector("static_constr_lazy"), [False]) + assert_equals(sample.get_vector("static_instance_features"), [67.0, 21.75]) + assert_equals(sample.get_scalar("static_constr_lazy_count"), 0) # after-lp # ------------------------------------------------------- From 067f0f847c8959087e377b3b080c18f9a3c17cb5 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 4 Aug 2021 13:38:23 -0500 Subject: [PATCH 37/67] Add mip_ prefix to dynamic constraints --- miplearn/components/dynamic_lazy.py | 4 ++-- miplearn/components/dynamic_user_cuts.py | 4 ++-- miplearn/components/static_lazy.py | 4 ++-- tests/components/test_dynamic_lazy.py | 6 +++--- tests/components/test_dynamic_user_cuts.py | 2 +- tests/components/test_static_lazy.py | 6 +++--- tests/problems/test_tsp.py | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/miplearn/components/dynamic_lazy.py b/miplearn/components/dynamic_lazy.py index a9145ec..9110524 100644 --- a/miplearn/components/dynamic_lazy.py +++ b/miplearn/components/dynamic_lazy.py @@ -36,7 +36,7 @@ class DynamicLazyConstraintsComponent(Component): self.dynamic: DynamicConstraintsComponent = DynamicConstraintsComponent( classifier=classifier, threshold=threshold, - attr="lazy_enforced", + attr="mip_constr_lazy_enforced", ) self.classifiers = self.dynamic.classifiers self.thresholds = self.dynamic.thresholds @@ -78,7 +78,7 @@ class DynamicLazyConstraintsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - sample.put_set("lazy_enforced", set(self.lazy_enforced)) + sample.put_set("mip_constr_lazy_enforced", set(self.lazy_enforced)) @overrides def iteration_cb( diff --git a/miplearn/components/dynamic_user_cuts.py b/miplearn/components/dynamic_user_cuts.py index b60cc44..5f69b04 100644 --- a/miplearn/components/dynamic_user_cuts.py +++ b/miplearn/components/dynamic_user_cuts.py @@ -32,7 +32,7 @@ class UserCutsComponent(Component): self.dynamic = DynamicConstraintsComponent( classifier=classifier, threshold=threshold, - attr="user_cuts_enforced", + attr="mip_user_cuts_enforced", ) self.enforced: Set[str] = set() self.n_added_in_callback = 0 @@ -87,7 +87,7 @@ class UserCutsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - sample.put_set("user_cuts_enforced", set(self.enforced)) + sample.put_set("mip_user_cuts_enforced", set(self.enforced)) stats["UserCuts: Added in callback"] = self.n_added_in_callback if self.n_added_in_callback > 0: logger.info(f"{self.n_added_in_callback} user cuts added in callback") diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 99aba1c..53a7a7d 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -61,7 +61,7 @@ class StaticLazyConstraintsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - sample.put_set("lazy_enforced", self.enforced_cids) + sample.put_set("mip_constr_lazy_enforced", self.enforced_cids) stats["LazyStatic: Restored"] = self.n_restored stats["LazyStatic: Iterations"] = self.n_iterations @@ -209,7 +209,7 @@ class StaticLazyConstraintsComponent(Component): constr_names = sample.get_vector("static_constr_names") constr_categories = sample.get_vector("static_constr_categories") constr_lazy = sample.get_vector("static_constr_lazy") - lazy_enforced = sample.get_set("lazy_enforced") + lazy_enforced = sample.get_set("mip_constr_lazy_enforced") if constr_features is None: constr_features = sample.get_vector_list("static_constr_features") diff --git a/tests/components/test_dynamic_lazy.py b/tests/components/test_dynamic_lazy.py index 037b904..cec993e 100644 --- a/tests/components/test_dynamic_lazy.py +++ b/tests/components/test_dynamic_lazy.py @@ -24,13 +24,13 @@ def training_instances() -> List[Instance]: samples_0 = [ MemorySample( { - "lazy_enforced": {"c1", "c2"}, + "mip_constr_lazy_enforced": {"c1", "c2"}, "static_instance_features": [5.0], }, ), MemorySample( { - "lazy_enforced": {"c2", "c3"}, + "mip_constr_lazy_enforced": {"c2", "c3"}, "static_instance_features": [5.0], }, ), @@ -55,7 +55,7 @@ def training_instances() -> List[Instance]: samples_1 = [ MemorySample( { - "lazy_enforced": {"c3", "c4"}, + "mip_constr_lazy_enforced": {"c3", "c4"}, "static_instance_features": [8.0], }, ) diff --git a/tests/components/test_dynamic_user_cuts.py b/tests/components/test_dynamic_user_cuts.py index cfb76c0..44205a6 100644 --- a/tests/components/test_dynamic_user_cuts.py +++ b/tests/components/test_dynamic_user_cuts.py @@ -81,7 +81,7 @@ def test_usage( ) -> None: stats_before = solver.solve(stab_instance) sample = stab_instance.get_samples()[0] - user_cuts_enforced = sample.get_set("user_cuts_enforced") + user_cuts_enforced = sample.get_set("mip_user_cuts_enforced") assert user_cuts_enforced is not None assert len(user_cuts_enforced) > 0 assert stats_before["UserCuts: Added ahead-of-time"] == 0 diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index 6ef4ec9..385c7cc 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -34,7 +34,7 @@ def sample() -> Sample: "static_constr_lazy": [True, True, True, True, False], "static_constr_names": ["c1", "c2", "c3", "c4", "c5"], "static_instance_features": [5.0], - "lazy_enforced": {"c1", "c2", "c4"}, + "mip_constr_lazy_enforced": {"c1", "c2", "c4"}, "lp_constr_features": [ [1.0, 1.0], [1.0, 2.0], @@ -93,7 +93,7 @@ def test_usage_with_solver(instance: Instance) -> None: stats: LearningSolveStats = {} sample = instance.get_samples()[0] - assert sample.get_set("lazy_enforced") is not None + assert sample.get_set("mip_constr_lazy_enforced") is not None # LearningSolver calls before_solve_mip component.before_solve_mip( @@ -142,7 +142,7 @@ def test_usage_with_solver(instance: Instance) -> None: ) # Should update training sample - assert sample.get_set("lazy_enforced") == {"c1", "c2", "c3", "c4"} + assert sample.get_set("mip_constr_lazy_enforced") == {"c1", "c2", "c3", "c4"} # # Should update stats assert stats["LazyStatic: Removed"] == 1 diff --git a/tests/problems/test_tsp.py b/tests/problems/test_tsp.py index 894fb57..48c9931 100644 --- a/tests/problems/test_tsp.py +++ b/tests/problems/test_tsp.py @@ -65,7 +65,7 @@ def test_subtour() -> None: samples = instance.get_samples() assert len(samples) == 1 sample = samples[0] - lazy_enforced = sample.get_set("lazy_enforced") + lazy_enforced = sample.get_set("mip_constr_lazy_enforced") assert lazy_enforced is not None assert len(lazy_enforced) > 0 assert sample.get_vector("mip_var_values") == [ From e72f3b553f64f274078f30f64527d8655cf53c42 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 4 Aug 2021 13:44:42 -0500 Subject: [PATCH 38/67] Hdf5Sample: Use half-precision for floats --- miplearn/features/sample.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 0dd6005..65ddab5 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -249,6 +249,7 @@ class Hdf5Sample(Sample): self._assert_is_vector(value) for v in value: + # Convert strings to bytes if isinstance(v, str): value = np.array( [u if u is not None else b"" for u in value], @@ -256,6 +257,11 @@ class Hdf5Sample(Sample): ) break + # Convert all floating point numbers to half-precision + if isinstance(v, float): + value = np.array(value, dtype=np.dtype("f2")) + break + self._put(key, value, compress=True) @overrides @@ -269,6 +275,8 @@ class Hdf5Sample(Sample): continue if isinstance(v[0], str): data = np.array(padded, dtype="S") + elif isinstance(v[0], float): + data = np.array(padded, dtype=np.dtype("f2")) elif isinstance(v[0], bool): data = np.array(padded, dtype=bool) else: From 4a529119246568b7dae70c91b5758fad94f08acd Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 4 Aug 2021 13:54:14 -0500 Subject: [PATCH 39/67] AlvLouWeh2017: Replace non-finite features by constant --- miplearn/features/extractor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 74c359d..6040527 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -320,8 +320,10 @@ class FeaturesExtractor: else: f.append(0.0) - for v in f: - assert isfinite(v), f"non-finite elements detected: {f}" + for (i, v) in enumerate(f): + if not isfinite(v): + f[i] = 0.0 + features.append(f) return features From 95b9ce29fd4ca97d5c6dbc3a64854c203f104464 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 5 Aug 2021 10:18:34 -0500 Subject: [PATCH 40/67] Hdf5Sample: Use latest HDF5 file format --- miplearn/features/sample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 65ddab5..422c979 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -176,7 +176,7 @@ class Hdf5Sample(Sample): """ def __init__(self, filename: str, mode: str = "r+") -> None: - self.file = h5py.File(filename, mode) + self.file = h5py.File(filename, mode, libver="latest") @overrides def get_bytes(self, key: str) -> Optional[Bytes]: From 475fe3d9859bdcbe073f64a397237d25414b55db Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Thu, 5 Aug 2021 12:33:36 -0500 Subject: [PATCH 41/67] Sample: do not check data by default; minor fixes --- miplearn/features/sample.py | 38 ++++++++++++++++++++++---------- tests/features/test_extractor.py | 35 +++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 422c979..11f37b4 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -110,10 +110,12 @@ class MemorySample(Sample): def __init__( self, data: Optional[Dict[str, Any]] = None, + check_data: bool = False, ) -> None: if data is None: data = {} self._data: Dict[str, Any] = data + self._check_data = check_data @overrides def get_bytes(self, key: str) -> Optional[Bytes]: @@ -142,19 +144,22 @@ class MemorySample(Sample): def put_scalar(self, key: str, value: Scalar) -> None: if value is None: return - self._assert_is_scalar(value) + if self._check_data: + self._assert_is_scalar(value) self._put(key, value) @overrides def put_vector(self, key: str, value: Vector) -> None: if value is None: return - self._assert_is_vector(value) + if self._check_data: + self._assert_is_vector(value) self._put(key, value) @overrides def put_vector_list(self, key: str, value: VectorList) -> None: - self._assert_is_vector_list(value) + if self._check_data: + self._assert_is_vector_list(value) self._put(key, value) def _get(self, key: str) -> Optional[Any]: @@ -175,8 +180,14 @@ class Hdf5Sample(Sample): are actually accessed, and therefore it is more scalable. """ - def __init__(self, filename: str, mode: str = "r+") -> None: + def __init__( + self, + filename: str, + mode: str = "r+", + check_data: bool = False, + ) -> None: self.file = h5py.File(filename, mode, libver="latest") + self._check_data = check_data @overrides def get_bytes(self, key: str) -> Optional[Bytes]: @@ -230,27 +241,30 @@ class Hdf5Sample(Sample): @overrides def put_bytes(self, key: str, value: Bytes) -> None: - assert isinstance( - value, (bytes, bytearray) - ), f"bytes expected; found: {value}" # type: ignore + if self._check_data: + assert isinstance( + value, (bytes, bytearray) + ), f"bytes expected; found: {value}" # type: ignore self._put(key, np.frombuffer(value, dtype="uint8"), compress=True) @overrides def put_scalar(self, key: str, value: Any) -> None: if value is None: return - self._assert_is_scalar(value) + if self._check_data: + self._assert_is_scalar(value) self._put(key, value) @overrides def put_vector(self, key: str, value: Vector) -> None: if value is None: return - self._assert_is_vector(value) + if self._check_data: + self._assert_is_vector(value) for v in value: # Convert strings to bytes - if isinstance(v, str): + if isinstance(v, str) or v is None: value = np.array( [u if u is not None else b"" for u in value], dtype="S", @@ -266,7 +280,8 @@ class Hdf5Sample(Sample): @overrides def put_vector_list(self, key: str, value: VectorList) -> None: - self._assert_is_vector_list(value) + if self._check_data: + self._assert_is_vector_list(value) padded, lens = _pad(value) self.put_vector(f"{key}_lengths", lens) data = None @@ -297,7 +312,6 @@ class Hdf5Sample(Sample): def _pad(veclist: VectorList) -> Tuple[VectorList, List[int]]: - veclist = deepcopy(veclist) lens = [len(v) if v is not None else -1 for v in veclist] maxlen = max(lens) diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 1053b4b..9edb496 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -2,13 +2,20 @@ # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. +import sys +import time +from typing import Any + import numpy as np +import gurobipy as gp from miplearn.features.extractor import FeaturesExtractor -from miplearn.features.sample import Sample, MemorySample -from miplearn.solvers.internal import Variables, Constraints +from miplearn.features.sample import MemorySample, Hdf5Sample +from miplearn.instance.base import Instance from miplearn.solvers.gurobi import GurobiSolver +from miplearn.solvers.internal import Variables, Constraints from miplearn.solvers.tests import assert_equals +import cProfile inf = float("inf") @@ -166,3 +173,27 @@ def test_assert_equals() -> None: assert_equals(np.array([True, True]), [True, True]) assert_equals((1.0,), (1.0,)) assert_equals({"x": 10}, {"x": 10}) + + +class MpsInstance(Instance): + def __init__(self, filename: str) -> None: + super().__init__() + self.filename = filename + + def to_model(self) -> Any: + return gp.read(self.filename) + + +if __name__ == "__main__": + solver = GurobiSolver() + instance = MpsInstance(sys.argv[1]) + solver.set_instance(instance) + solver.solve_lp(tee=True) + extractor = FeaturesExtractor(with_lhs=False) + sample = Hdf5Sample("tmp/prof.h5", mode="w") + + def run(): + extractor.extract_after_load_features(instance, solver, sample) + extractor.extract_after_lp_features(solver, sample) + + cProfile.run("run()", filename="tmp/prof") From b6426462a116f3fb0187a1ccdae3d258e5ace6fe Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 5 Aug 2021 14:05:50 -0500 Subject: [PATCH 42/67] Fix failing tests --- miplearn/features/sample.py | 1 + tests/features/test_extractor.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 11f37b4..4f33edd 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -312,6 +312,7 @@ class Hdf5Sample(Sample): def _pad(veclist: VectorList) -> Tuple[VectorList, List[int]]: + veclist = deepcopy(veclist) lens = [len(v) if v is not None else -1 for v in veclist] maxlen = max(lens) diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 9edb496..9612211 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -192,7 +192,7 @@ if __name__ == "__main__": extractor = FeaturesExtractor(with_lhs=False) sample = Hdf5Sample("tmp/prof.h5", mode="w") - def run(): + def run() -> None: extractor.extract_after_load_features(instance, solver, sample) extractor.extract_after_lp_features(solver, sample) From 0c4b0ea81af12feb1d9702ce30f81f5e67933bb4 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 5 Aug 2021 15:42:19 -0500 Subject: [PATCH 43/67] Use np.ndarray in Variables --- miplearn/solvers/__init__.py | 7 +++- miplearn/solvers/gurobi.py | 56 ++++++++++++++++++++------- miplearn/solvers/internal.py | 24 ++++++------ miplearn/solvers/pyomo/base.py | 10 ++--- miplearn/solvers/tests/__init__.py | 28 ++++++++------ tests/components/test_primal.py | 2 +- tests/features/test_extractor.py | 3 +- tests/instance/test_file.py | 2 +- tests/problems/test_tsp.py | 40 ++++++++++--------- tests/solvers/test_learning_solver.py | 4 +- 10 files changed, 110 insertions(+), 66 deletions(-) diff --git a/miplearn/solvers/__init__.py b/miplearn/solvers/__init__.py index e172895..060153c 100644 --- a/miplearn/solvers/__init__.py +++ b/miplearn/solvers/__init__.py @@ -4,7 +4,7 @@ import logging import sys -from typing import Any, List, TextIO, cast +from typing import Any, List, TextIO, cast, TypeVar, Optional, Sized logger = logging.getLogger(__name__) @@ -38,7 +38,10 @@ class _RedirectOutput: sys.stderr = self._original_stderr -def _none_if_empty(obj: Any) -> Any: +T = TypeVar("T", bound=Sized) + + +def _none_if_empty(obj: T) -> Optional[T]: if len(obj) == 0: return None else: diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index 20982f9..d8761b9 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -6,8 +6,9 @@ import re import sys from io import StringIO from random import randint -from typing import List, Any, Dict, Optional, Tuple, TYPE_CHECKING +from typing import List, Any, Dict, Optional, TYPE_CHECKING +import numpy as np from overrides import overrides from miplearn.instance.base import Instance @@ -79,9 +80,9 @@ class GurobiSolver(InternalSolver): self._var_names: List[str] = [] self._constr_names: List[str] = [] self._var_types: List[str] = [] - self._var_lbs: List[float] = [] - self._var_ubs: List[float] = [] - self._var_obj_coeffs: List[float] = [] + self._var_lbs: np.ndarray = np.empty(0) + self._var_ubs: np.ndarray = np.empty(0) + self._var_obj_coeffs: np.ndarray = np.empty(0) if self.lazy_cb_frequency == 1: self.lazy_cb_where = [self.gp.GRB.Callback.MIPSOL] @@ -338,15 +339,33 @@ class GurobiSolver(InternalSolver): ) if with_sa: - 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) + sa_obj_up = np.array( + model.getAttr("saobjUp", self._gp_vars), + dtype=float, + ) + sa_obj_down = np.array( + model.getAttr("saobjLow", self._gp_vars), + dtype=float, + ) + sa_ub_up = np.array( + model.getAttr("saubUp", self._gp_vars), + dtype=float, + ) + sa_ub_down = np.array( + model.getAttr("saubLow", self._gp_vars), + dtype=float, + ) + sa_lb_up = np.array( + model.getAttr("salbUp", self._gp_vars), + dtype=float, + ) + sa_lb_down = np.array( + model.getAttr("salbLow", self._gp_vars), + dtype=float, + ) if model.solCount > 0: - values = model.getAttr("x", self._gp_vars) + values = np.array(model.getAttr("x", self._gp_vars), dtype=float) return Variables( names=self._var_names, @@ -565,9 +584,18 @@ class GurobiSolver(InternalSolver): gp_constrs: List["gurobipy.Constr"] = self.model.getConstrs() var_names: List[str] = self.model.getAttr("varName", gp_vars) var_types: List[str] = self.model.getAttr("vtype", gp_vars) - var_ubs: List[float] = self.model.getAttr("ub", gp_vars) - var_lbs: List[float] = self.model.getAttr("lb", gp_vars) - var_obj_coeffs: List[float] = self.model.getAttr("obj", gp_vars) + var_ubs: np.ndarray = np.array( + self.model.getAttr("ub", gp_vars), + dtype=float, + ) + var_lbs: np.ndarray = np.array( + self.model.getAttr("lb", gp_vars), + dtype=float, + ) + var_obj_coeffs: np.ndarray = np.array( + self.model.getAttr("obj", gp_vars), + dtype=float, + ) constr_names: List[str] = self.model.getAttr("constrName", gp_constrs) varname_to_var: Dict = {} cname_to_constr: Dict = {} diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index c462797..c8dbd84 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -7,6 +7,8 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any, Optional, List, Tuple, TYPE_CHECKING +import numpy as np + from miplearn.instance.base import Instance from miplearn.types import ( IterationCallback, @@ -50,18 +52,18 @@ class MIPSolveStats: class Variables: names: Optional[List[str]] = None basis_status: Optional[List[str]] = 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 + lower_bounds: Optional[np.ndarray] = None + obj_coeffs: Optional[np.ndarray] = None + reduced_costs: Optional[np.ndarray] = None + sa_lb_down: Optional[np.ndarray] = None + sa_lb_up: Optional[np.ndarray] = None + sa_obj_down: Optional[np.ndarray] = None + sa_obj_up: Optional[np.ndarray] = None + sa_ub_down: Optional[np.ndarray] = None + sa_ub_up: Optional[np.ndarray] = None types: Optional[List[str]] = None - upper_bounds: Optional[List[float]] = None - values: Optional[List[float]] = None + upper_bounds: Optional[np.ndarray] = None + values: Optional[np.ndarray] = None @dataclass diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index d0ffb3c..698c5a4 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -330,11 +330,11 @@ class BasePyomoSolver(InternalSolver): return Variables( 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), + upper_bounds=_none_if_empty(np.array(upper_bounds, dtype=float)), + lower_bounds=_none_if_empty(np.array(lower_bounds, dtype=float)), + obj_coeffs=_none_if_empty(np.array(obj_coeffs, dtype=float)), + reduced_costs=_none_if_empty(np.array(reduced_costs, dtype=float)), + values=_none_if_empty(np.array(values, dtype=float)), ) @overrides diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index 4626b4d..b425b0d 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -41,10 +41,10 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_variables(), Variables( 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], + lower_bounds=np.array([0.0, 0.0, 0.0, 0.0, 0.0]), + upper_bounds=np.array([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], + obj_coeffs=np.array([505.0, 352.0, 458.0, 220.0, 0.0]), ), ) @@ -85,14 +85,18 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: Variables( 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], + reduced_costs=np.array( + [193.615385, 0.0, 187.230769, -23.692308, 13.538462] + ), + sa_lb_down=np.array([-inf, -inf, -inf, -0.111111, -inf]), + sa_lb_up=np.array([1.0, 0.923077, 1.0, 1.0, 67.0]), + sa_obj_down=np.array( + [311.384615, 317.777778, 270.769231, -inf, -13.538462] + ), + sa_obj_up=np.array([inf, 570.869565, inf, 243.692308, inf]), + sa_ub_down=np.array([0.913043, 0.923077, 0.9, 0.0, 43.0]), + sa_ub_up=np.array([2.043478, inf, 2.2, inf, 69.0]), + values=np.array([1.0, 0.923077, 1.0, 0.0, 67.0]), ), ), ) @@ -137,7 +141,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_variable_attrs(), Variables( names=["x[0]", "x[1]", "x[2]", "x[3]", "z"], - values=[1.0, 0.0, 1.0, 1.0, 61.0], + values=np.array([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 a5958ac..7672011 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -24,7 +24,7 @@ def sample() -> Sample: { "static_var_names": ["x[0]", "x[1]", "x[2]", "x[3]"], "static_var_categories": ["default", None, "default", "default"], - "mip_var_values": [0.0, 1.0, 1.0, 0.0], + "mip_var_values": np.array([0.0, 1.0, 1.0, 0.0]), "static_instance_features": [5.0], "static_var_features": [ [0.0, 0.0], diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 9612211..d6ce933 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -32,7 +32,8 @@ def test_knapsack() -> None: # ------------------------------------------------------- extractor.extract_after_load_features(instance, solver, sample) assert_equals( - sample.get_vector("static_var_names"), ["x[0]", "x[1]", "x[2]", "x[3]", "z"] + sample.get_vector("static_var_names"), + ["x[0]", "x[1]", "x[2]", "x[3]", "z"], ) assert_equals( sample.get_vector("static_var_lower_bounds"), [0.0, 0.0, 0.0, 0.0, 0.0] diff --git a/tests/instance/test_file.py b/tests/instance/test_file.py index 6ff9767..5cf4d22 100644 --- a/tests/instance/test_file.py +++ b/tests/instance/test_file.py @@ -17,7 +17,7 @@ def test_usage() -> None: # Save instance to disk filename = tempfile.mktemp() FileInstance.save(original, filename) - sample = Hdf5Sample(filename) + sample = Hdf5Sample(filename, check_data=True) assert len(sample.get_bytes("pickled")) > 0 # Solve instance from disk diff --git a/tests/problems/test_tsp.py b/tests/problems/test_tsp.py index 48c9931..4a1079b 100644 --- a/tests/problems/test_tsp.py +++ b/tests/problems/test_tsp.py @@ -9,6 +9,7 @@ from scipy.stats import uniform, randint from miplearn.problems.tsp import TravelingSalesmanGenerator, TravelingSalesmanInstance from miplearn.solvers.learning import LearningSolver +from miplearn.solvers.tests import assert_equals def test_generator() -> None: @@ -41,7 +42,7 @@ def test_instance() -> None: solver.solve(instance) assert len(instance.get_samples()) == 1 sample = instance.get_samples()[0] - assert sample.get_vector("mip_var_values") == [1.0, 0.0, 1.0, 1.0, 0.0, 1.0] + assert_equals(sample.get_vector("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 0.0, 1.0]) assert sample.get_scalar("mip_lower_bound") == 4.0 assert sample.get_scalar("mip_upper_bound") == 4.0 @@ -68,22 +69,25 @@ def test_subtour() -> None: lazy_enforced = sample.get_set("mip_constr_lazy_enforced") assert lazy_enforced is not None assert len(lazy_enforced) > 0 - assert sample.get_vector("mip_var_values") == [ - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 1.0, - 1.0, - ] + assert_equals( + sample.get_vector("mip_var_values"), + [ + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 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 add4507..bc4db78 100644 --- a/tests/solvers/test_learning_solver.py +++ b/tests/solvers/test_learning_solver.py @@ -38,7 +38,9 @@ def test_learning_solver( assert len(instance.get_samples()) > 0 sample = instance.get_samples()[0] - assert sample.get_vector("mip_var_values") == [1.0, 0.0, 1.0, 1.0, 61.0] + assert_equals( + sample.get_vector("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 61.0] + ) assert sample.get_scalar("mip_lower_bound") == 1183.0 assert sample.get_scalar("mip_upper_bound") == 1183.0 mip_log = sample.get_scalar("mip_log") From 0a32586bf8b4db110058eb026bc1263b643817c3 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 5 Aug 2021 15:57:02 -0500 Subject: [PATCH 44/67] Use np.ndarray in Constraints --- miplearn/solvers/gurobi.py | 12 +++++++----- miplearn/solvers/internal.py | 24 ++++++++++++++---------- miplearn/solvers/pyomo/base.py | 6 +++--- miplearn/solvers/tests/__init__.py | 18 +++++++++--------- tests/features/test_extractor.py | 4 ++-- 5 files changed, 35 insertions(+), 29 deletions(-) diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index d8761b9..ba96463 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -217,7 +217,7 @@ class GurobiSolver(InternalSolver): dual_value, basis_status, sa_rhs_up, sa_rhs_down = None, None, None, None if with_static: - rhs = model.getAttr("rhs", gp_constrs) + rhs = np.array(model.getAttr("rhs", gp_constrs), dtype=float) senses = model.getAttr("sense", gp_constrs) if with_lhs: lhs = [None for _ in gp_constrs] @@ -229,7 +229,7 @@ class GurobiSolver(InternalSolver): ] if self._has_lp_solution: - dual_value = model.getAttr("pi", gp_constrs) + dual_value = np.array(model.getAttr("pi", gp_constrs), dtype=float) basis_status = list( map( _parse_gurobi_cbasis, @@ -237,11 +237,13 @@ class GurobiSolver(InternalSolver): ) ) if with_sa: - sa_rhs_up = model.getAttr("saRhsUp", gp_constrs) - sa_rhs_down = model.getAttr("saRhsLow", gp_constrs) + sa_rhs_up = np.array(model.getAttr("saRhsUp", gp_constrs), dtype=float) + sa_rhs_down = np.array( + model.getAttr("saRhsLow", gp_constrs), dtype=float + ) if self._has_lp_solution or self._has_mip_solution: - slacks = model.getAttr("slack", gp_constrs) + slacks = np.array(model.getAttr("slack", gp_constrs), dtype=float) return Constraints( basis_status=basis_status, diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index c8dbd84..5754aa9 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -69,15 +69,15 @@ class Variables: @dataclass class Constraints: basis_status: Optional[List[str]] = None - dual_values: Optional[List[float]] = None + dual_values: Optional[np.ndarray] = None lazy: Optional[List[bool]] = None lhs: Optional[List[List[Tuple[str, float]]]] = None names: Optional[List[str]] = None - rhs: Optional[List[float]] = None - sa_rhs_down: Optional[List[float]] = None - sa_rhs_up: Optional[List[float]] = None + rhs: Optional[np.ndarray] = None + sa_rhs_down: Optional[np.ndarray] = None + sa_rhs_up: Optional[np.ndarray] = None senses: Optional[List[str]] = None - slacks: Optional[List[float]] = None + slacks: Optional[np.ndarray] = None @staticmethod def from_sample(sample: "Sample") -> "Constraints": @@ -97,15 +97,19 @@ class Constraints: def __getitem__(self, selected: List[bool]) -> "Constraints": return Constraints( basis_status=self._filter(self.basis_status, selected), - dual_values=self._filter(self.dual_values, selected), + dual_values=( + None if self.dual_values is None else self.dual_values[selected] + ), names=self._filter(self.names, selected), lazy=self._filter(self.lazy, selected), lhs=self._filter(self.lhs, selected), - rhs=self._filter(self.rhs, selected), - sa_rhs_down=self._filter(self.sa_rhs_down, selected), - sa_rhs_up=self._filter(self.sa_rhs_up, selected), + rhs=(None if self.rhs is None else self.rhs[selected]), + sa_rhs_down=( + None if self.sa_rhs_down is None else self.sa_rhs_down[selected] + ), + sa_rhs_up=(None if self.sa_rhs_up is None else self.sa_rhs_up[selected]), senses=self._filter(self.senses, selected), - slacks=self._filter(self.slacks, selected), + slacks=(None if self.slacks is None else self.slacks[selected]), ) def _filter( diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index 698c5a4..b3640ec 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -236,11 +236,11 @@ class BasePyomoSolver(InternalSolver): return Constraints( names=_none_if_empty(names), - rhs=_none_if_empty(rhs), + rhs=_none_if_empty(np.array(rhs, dtype=float)), senses=_none_if_empty(senses), lhs=_none_if_empty(lhs), - slacks=_none_if_empty(slacks), - dual_values=_none_if_empty(dual_values), + slacks=_none_if_empty(np.array(slacks, dtype=float)), + dual_values=_none_if_empty(np.array(dual_values, dtype=float)), ) @overrides diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index b425b0d..c92b07b 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -53,7 +53,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_constraints(), Constraints( names=["eq_capacity"], - rhs=[0.0], + rhs=np.array([0.0]), lhs=[ [ ("x[0]", 23.0), @@ -108,11 +108,11 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_constraint_attrs(), Constraints( basis_status=["N"], - dual_values=[13.538462], + dual_values=np.array([13.538462]), names=["eq_capacity"], - sa_rhs_down=[-24.0], - sa_rhs_up=[2.0], - slacks=[0.0], + sa_rhs_down=np.array([-24.0]), + sa_rhs_up=np.array([2.0]), + slacks=np.array([0.0]), ), ), ) @@ -153,7 +153,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_constraint_attrs(), Constraints( names=["eq_capacity"], - slacks=[0.0], + slacks=np.array([0.0]), ), ), ) @@ -162,7 +162,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: cf = Constraints( names=["cut"], lhs=[[("x[0]", 1.0)]], - rhs=[0.0], + rhs=np.array([0.0]), senses=["<"], ) assert_equals(solver.are_constraints_satisfied(cf), [False]) @@ -175,7 +175,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_constraint_attrs(), Constraints( names=["eq_capacity", "cut"], - rhs=[0.0, 0.0], + rhs=np.array([0.0, 0.0]), lhs=[ [ ("x[0]", 23.0), @@ -274,7 +274,7 @@ def _equals_preprocess(obj: Any) -> Any: return np.round(obj, decimals=6).tolist() else: return obj.tolist() - elif isinstance(obj, (int, str)): + elif isinstance(obj, (int, str, bool, np.bool_)): return obj elif isinstance(obj, float): return round(obj, 6) diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index d6ce933..2fc1633 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -122,7 +122,7 @@ def test_knapsack() -> None: def test_constraint_getindex() -> None: cf = Constraints( names=["c1", "c2", "c3"], - rhs=[1.0, 2.0, 3.0], + rhs=np.array([1.0, 2.0, 3.0]), senses=["=", "<", ">"], lhs=[ [ @@ -143,7 +143,7 @@ def test_constraint_getindex() -> None: cf[[True, False, True]], Constraints( names=["c1", "c3"], - rhs=[1.0, 3.0], + rhs=np.array([1.0, 3.0]), senses=["=", ">"], lhs=[ [ From f69067aafdb06d262e45a448d26ac9cb2b7052d5 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Sun, 8 Aug 2021 06:52:24 -0500 Subject: [PATCH 45/67] Implement {get,put}_array; make other methods deprecated --- miplearn/features/extractor.py | 66 +++++++-------- miplearn/features/sample.py | 53 ++++++++++-- miplearn/solvers/gurobi.py | 2 +- tests/features/test_sample.py | 144 ++++----------------------------- 4 files changed, 98 insertions(+), 167 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 6040527..8848e80 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -33,14 +33,14 @@ class FeaturesExtractor: ) -> None: variables = solver.get_variables(with_static=True) constraints = solver.get_constraints(with_static=True, with_lhs=self.with_lhs) - sample.put_vector("static_var_lower_bounds", variables.lower_bounds) + sample.put_array("static_var_lower_bounds", variables.lower_bounds) sample.put_vector("static_var_names", variables.names) - sample.put_vector("static_var_obj_coeffs", variables.obj_coeffs) + sample.put_array("static_var_obj_coeffs", variables.obj_coeffs) sample.put_vector("static_var_types", variables.types) - sample.put_vector("static_var_upper_bounds", variables.upper_bounds) + sample.put_array("static_var_upper_bounds", variables.upper_bounds) sample.put_vector("static_constr_names", constraints.names) # sample.put("static_constr_lhs", constraints.lhs) - sample.put_vector("static_constr_rhs", constraints.rhs) + sample.put_array("static_constr_rhs", constraints.rhs) sample.put_vector("static_constr_senses", constraints.senses) vars_features_user, var_categories = self._extract_user_features_vars( instance, sample @@ -55,9 +55,9 @@ class FeaturesExtractor: [ alw17, vars_features_user, - sample.get_vector("static_var_lower_bounds"), - sample.get_vector("static_var_obj_coeffs"), - sample.get_vector("static_var_upper_bounds"), + sample.get_array("static_var_lower_bounds"), + sample.get_array("static_var_obj_coeffs"), + sample.get_array("static_var_upper_bounds"), ], ), ) @@ -70,33 +70,33 @@ class FeaturesExtractor: variables = solver.get_variables(with_static=False, with_sa=self.with_sa) constraints = solver.get_constraints(with_static=False, with_sa=self.with_sa) sample.put_vector("lp_var_basis_status", variables.basis_status) - sample.put_vector("lp_var_reduced_costs", variables.reduced_costs) - sample.put_vector("lp_var_sa_lb_down", variables.sa_lb_down) - sample.put_vector("lp_var_sa_lb_up", variables.sa_lb_up) - sample.put_vector("lp_var_sa_obj_down", variables.sa_obj_down) - sample.put_vector("lp_var_sa_obj_up", variables.sa_obj_up) - sample.put_vector("lp_var_sa_ub_down", variables.sa_ub_down) - sample.put_vector("lp_var_sa_ub_up", variables.sa_ub_up) - sample.put_vector("lp_var_values", variables.values) + sample.put_array("lp_var_reduced_costs", variables.reduced_costs) + sample.put_array("lp_var_sa_lb_down", variables.sa_lb_down) + sample.put_array("lp_var_sa_lb_up", variables.sa_lb_up) + sample.put_array("lp_var_sa_obj_down", variables.sa_obj_down) + sample.put_array("lp_var_sa_obj_up", variables.sa_obj_up) + sample.put_array("lp_var_sa_ub_down", variables.sa_ub_down) + sample.put_array("lp_var_sa_ub_up", variables.sa_ub_up) + sample.put_array("lp_var_values", variables.values) sample.put_vector("lp_constr_basis_status", constraints.basis_status) - sample.put_vector("lp_constr_dual_values", constraints.dual_values) - sample.put_vector("lp_constr_sa_rhs_down", constraints.sa_rhs_down) - sample.put_vector("lp_constr_sa_rhs_up", constraints.sa_rhs_up) - sample.put_vector("lp_constr_slacks", constraints.slacks) + sample.put_array("lp_constr_dual_values", constraints.dual_values) + sample.put_array("lp_constr_sa_rhs_down", constraints.sa_rhs_down) + sample.put_array("lp_constr_sa_rhs_up", constraints.sa_rhs_up) + sample.put_array("lp_constr_slacks", constraints.slacks) alw17 = self._extract_var_features_AlvLouWeh2017(sample) sample.put_vector_list( "lp_var_features", self._combine( [ alw17, - sample.get_vector("lp_var_reduced_costs"), - sample.get_vector("lp_var_sa_lb_down"), - sample.get_vector("lp_var_sa_lb_up"), - sample.get_vector("lp_var_sa_obj_down"), - sample.get_vector("lp_var_sa_obj_up"), - sample.get_vector("lp_var_sa_ub_down"), - sample.get_vector("lp_var_sa_ub_up"), - sample.get_vector("lp_var_values"), + sample.get_array("lp_var_reduced_costs"), + sample.get_array("lp_var_sa_lb_down"), + sample.get_array("lp_var_sa_lb_up"), + sample.get_array("lp_var_sa_obj_down"), + sample.get_array("lp_var_sa_obj_up"), + sample.get_array("lp_var_sa_ub_down"), + sample.get_array("lp_var_sa_ub_up"), + sample.get_array("lp_var_values"), sample.get_vector_list("static_var_features"), ], ), @@ -106,10 +106,10 @@ class FeaturesExtractor: self._combine( [ sample.get_vector_list("static_constr_features"), - sample.get_vector("lp_constr_dual_values"), - sample.get_vector("lp_constr_sa_rhs_down"), - sample.get_vector("lp_constr_sa_rhs_up"), - sample.get_vector("lp_constr_slacks"), + sample.get_array("lp_constr_dual_values"), + sample.get_array("lp_constr_sa_rhs_down"), + sample.get_array("lp_constr_sa_rhs_up"), + sample.get_array("lp_constr_slacks"), ], ), ) @@ -131,8 +131,8 @@ class FeaturesExtractor: ) -> None: variables = solver.get_variables(with_static=False, with_sa=False) constraints = solver.get_constraints(with_static=False, with_sa=False) - sample.put_vector("mip_var_values", variables.values) - sample.put_vector("mip_constr_slacks", constraints.slacks) + sample.put_array("mip_var_values", variables.values) + sample.put_array("mip_constr_slacks", constraints.slacks) def _extract_user_features_vars( self, diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 4f33edd..10ff3e5 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -1,7 +1,7 @@ # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. - +import warnings from abc import ABC, abstractmethod from copy import deepcopy from typing import Dict, Optional, Any, Union, List, Tuple, cast, Set @@ -39,11 +39,12 @@ class Sample(ABC): @abstractmethod def get_bytes(self, key: str) -> Optional[Bytes]: - pass + warnings.warn("Deprecated", DeprecationWarning) + return None @abstractmethod def put_bytes(self, key: str, value: Bytes) -> None: - pass + warnings.warn("Deprecated", DeprecationWarning) @abstractmethod def get_scalar(self, key: str) -> Optional[Any]: @@ -55,18 +56,28 @@ class Sample(ABC): @abstractmethod def get_vector(self, key: str) -> Optional[Any]: - pass + warnings.warn("Deprecated", DeprecationWarning) + return None @abstractmethod def put_vector(self, key: str, value: Vector) -> None: - pass + warnings.warn("Deprecated", DeprecationWarning) @abstractmethod def get_vector_list(self, key: str) -> Optional[Any]: - pass + warnings.warn("Deprecated", DeprecationWarning) + return None @abstractmethod def put_vector_list(self, key: str, value: VectorList) -> None: + warnings.warn("Deprecated", DeprecationWarning) + + @abstractmethod + def put_array(self, key: str, value: Optional[np.ndarray]) -> None: + pass + + @abstractmethod + def get_array(self, key: str) -> Optional[np.ndarray]: pass def get_set(self, key: str) -> Set: @@ -103,6 +114,10 @@ class Sample(ABC): continue self._assert_is_vector(v) + def _assert_supported(self, value: np.ndarray) -> None: + assert isinstance(value, np.ndarray) + assert value.dtype.kind in "biufS", f"Unsupported dtype: {value.dtype}" + class MemorySample(Sample): """Dictionary-like class that stores training data in-memory.""" @@ -171,6 +186,17 @@ class MemorySample(Sample): def _put(self, key: str, value: Any) -> None: self._data[key] = value + @overrides + def put_array(self, key: str, value: Optional[np.ndarray]) -> None: + if value is None: + return + self._assert_supported(value) + self._put(key, value) + + @overrides + def get_array(self, key: str) -> Optional[np.ndarray]: + return cast(Optional[np.ndarray], self._get(key)) + class Hdf5Sample(Sample): """ @@ -310,6 +336,21 @@ class Hdf5Sample(Sample): ds = self.file.create_dataset(key, data=value) return ds + @overrides + def put_array(self, key: str, value: Optional[np.ndarray]) -> None: + if value is None: + return + self._assert_supported(value) + if key in self.file: + del self.file[key] + return self.file.create_dataset(key, data=value, compression="gzip") + + @overrides + def get_array(self, key: str) -> Optional[np.ndarray]: + if key not in self.file: + return None + return self.file[key][:] + def _pad(veclist: VectorList) -> Tuple[VectorList, List[int]]: veclist = deepcopy(veclist) diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index ba96463..724560f 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -332,7 +332,7 @@ class GurobiSolver(InternalSolver): obj_coeffs = self._var_obj_coeffs if self._has_lp_solution: - reduced_costs = model.getAttr("rc", self._gp_vars) + reduced_costs = np.array(model.getAttr("rc", self._gp_vars), dtype=float) basis_status = list( map( _parse_gurobi_vbasis, diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index a60e87b..9727713 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -3,10 +3,10 @@ # Released under the modified BSD license. See COPYING.md for more details. from tempfile import NamedTemporaryFile from typing import Any + import numpy as np -from miplearn.features.sample import MemorySample, Sample, Hdf5Sample, _pad, _crop -from miplearn.solvers.tests import assert_equals +from miplearn.features.sample import MemorySample, Sample, Hdf5Sample def test_memory_sample() -> None: @@ -19,54 +19,29 @@ def test_hdf5_sample() -> None: def _test_sample(sample: Sample) -> None: - # Scalar _assert_roundtrip_scalar(sample, "A") _assert_roundtrip_scalar(sample, True) _assert_roundtrip_scalar(sample, 1) _assert_roundtrip_scalar(sample, 1.0) - - # Vector - _assert_roundtrip_vector(sample, ["A", "BB", "CCC", None]) - _assert_roundtrip_vector(sample, [True, True, False]) - _assert_roundtrip_vector(sample, [1, 2, 3]) - _assert_roundtrip_vector(sample, [1.0, 2.0, 3.0]) - _assert_roundtrip_vector(sample, np.array([1.0, 2.0, 3.0]), check_type=False) - - # VectorList - _assert_roundtrip_vector_list(sample, [["A"], ["BB", "CCC"], None]) - _assert_roundtrip_vector_list(sample, [[True], [False, False], None]) - _assert_roundtrip_vector_list(sample, [[1], None, [2, 2], [3, 3, 3]]) - _assert_roundtrip_vector_list(sample, [[1.0], None, [2.0, 2.0], [3.0, 3.0, 3.0]]) - _assert_roundtrip_vector_list(sample, [None, None]) - - # Bytes - _assert_roundtrip_bytes(sample, b"\x00\x01\x02\x03\x04\x05") - _assert_roundtrip_bytes( - sample, - bytearray(b"\x00\x01\x02\x03\x04\x05"), - check_type=False, - ) - - # Querying unknown keys should return None + _assert_roundtrip_array(sample, np.array([True, False], dtype="bool")) + _assert_roundtrip_array(sample, np.array([1, 2, 3], dtype="int16")) + _assert_roundtrip_array(sample, np.array([1, 2, 3], dtype="int32")) + _assert_roundtrip_array(sample, np.array([1, 2, 3], dtype="int64")) + _assert_roundtrip_array(sample, np.array([1.0, 2.0, 3.0], dtype="float16")) + _assert_roundtrip_array(sample, np.array([1.0, 2.0, 3.0], dtype="float32")) + _assert_roundtrip_array(sample, np.array([1.0, 2.0, 3.0], dtype="float64")) + _assert_roundtrip_array(sample, np.array(["A", "BB", "CCC"], dtype="S")) assert sample.get_scalar("unknown-key") is None - assert sample.get_vector("unknown-key") is None - assert sample.get_vector_list("unknown-key") is None - assert sample.get_bytes("unknown-key") is None - - # Putting None should not modify HDF5 file - sample.put_scalar("key", None) - sample.put_vector("key", None) + assert sample.get_array("unknown-key") is None -def _assert_roundtrip_bytes( - sample: Sample, expected: Any, check_type: bool = False -) -> None: - sample.put_bytes("key", expected) - actual = sample.get_bytes("key") - assert actual == expected +def _assert_roundtrip_array(sample: Sample, expected: Any) -> None: + sample.put_array("key", expected) + actual = sample.get_array("key") assert actual is not None - if check_type: - _assert_same_type(actual, expected) + assert isinstance(actual, np.ndarray) + assert actual.dtype == expected.dtype + assert (actual == expected).all() def _assert_roundtrip_scalar(sample: Sample, expected: Any) -> None: @@ -74,91 +49,6 @@ def _assert_roundtrip_scalar(sample: Sample, expected: Any) -> None: actual = sample.get_scalar("key") assert actual == expected assert actual is not None - _assert_same_type(actual, expected) - - -def _assert_roundtrip_vector( - sample: Sample, expected: Any, check_type: bool = True -) -> None: - sample.put_vector("key", expected) - actual = sample.get_vector("key") - assert_equals(actual, expected) - assert actual is not None - if check_type: - _assert_same_type(actual[0], expected[0]) - - -def _assert_roundtrip_vector_list(sample: Sample, expected: Any) -> None: - sample.put_vector_list("key", expected) - actual = sample.get_vector_list("key") - assert actual == expected - assert actual is not None - if actual[0] is not None: - _assert_same_type(actual[0][0], expected[0][0]) - - -def _assert_same_type(actual: Any, expected: Any) -> None: assert isinstance( actual, expected.__class__ ), f"Expected {expected.__class__}, found {actual.__class__} instead" - - -def test_pad_int() -> None: - _assert_roundtrip_pad( - original=[[1], [2, 2, 2], [], [3, 3], [4, 4, 4, 4], None], - expected_padded=[ - [1, 0, 0, 0], - [2, 2, 2, 0], - [0, 0, 0, 0], - [3, 3, 0, 0], - [4, 4, 4, 4], - [0, 0, 0, 0], - ], - expected_lens=[1, 3, 0, 2, 4, -1], - dtype=int, - ) - - -def test_pad_float() -> None: - _assert_roundtrip_pad( - original=[[1.0], [2.0, 2.0, 2.0], [3.0, 3.0], [4.0, 4.0, 4.0, 4.0], None], - expected_padded=[ - [1.0, 0.0, 0.0, 0.0], - [2.0, 2.0, 2.0, 0.0], - [3.0, 3.0, 0.0, 0.0], - [4.0, 4.0, 4.0, 4.0], - [0.0, 0.0, 0.0, 0.0], - ], - expected_lens=[1, 3, 2, 4, -1], - dtype=float, - ) - - -def test_pad_str() -> None: - _assert_roundtrip_pad( - original=[["A"], ["B", "B", "B"], ["C", "C"]], - expected_padded=[["A", "", ""], ["B", "B", "B"], ["C", "C", ""]], - expected_lens=[1, 3, 2], - dtype=str, - ) - - -def _assert_roundtrip_pad( - original: Any, - expected_padded: Any, - expected_lens: Any, - dtype: Any, -) -> None: - actual_padded, actual_lens = _pad(original) - assert actual_padded == expected_padded - assert actual_lens == expected_lens - for v in actual_padded: - for vi in v: # type: ignore - assert isinstance(vi, dtype) - cropped = _crop(actual_padded, actual_lens) - assert cropped == original - for v in cropped: - if v is None: - continue - for vi in v: # type: ignore - assert isinstance(vi, dtype) From 7d55d6f34c83837a22f473768553ef3b5746f7aa Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Sun, 8 Aug 2021 07:24:14 -0500 Subject: [PATCH 46/67] Use np.array for Variables.names --- Makefile | 2 +- miplearn/components/primal.py | 8 +++--- miplearn/features/extractor.py | 31 +++++++++++++++----- miplearn/solvers/gurobi.py | 19 ++++++++----- miplearn/solvers/internal.py | 4 +-- miplearn/solvers/pyomo/base.py | 20 ++++++------- miplearn/solvers/tests/__init__.py | 45 +++++++++++++++--------------- miplearn/types.py | 3 +- tests/components/test_primal.py | 18 ++++++------ tests/features/test_extractor.py | 22 +++++++-------- 10 files changed, 96 insertions(+), 76 deletions(-) diff --git a/Makefile b/Makefile index 1341104..9a6c54a 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PYTHON := python3 PYTEST := pytest PIP := $(PYTHON) -m pip MYPY := $(PYTHON) -m mypy -PYTEST_ARGS := -W ignore::DeprecationWarning -vv -x --log-level=DEBUG +PYTEST_ARGS := -W ignore::DeprecationWarning -vv --log-level=DEBUG VERSION := 0.2 all: docs test diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index 50c7991..c08d5cb 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -95,7 +95,7 @@ class PrimalSolutionComponent(Component): ) def sample_predict(self, sample: Sample) -> Solution: - var_names = sample.get_vector("static_var_names") + var_names = sample.get_array("static_var_names") var_categories = sample.get_vector("static_var_categories") assert var_names is not None assert var_categories is not None @@ -145,7 +145,7 @@ class PrimalSolutionComponent(Component): instance_features = sample.get_vector("static_instance_features") mip_var_values = sample.get_vector("mip_var_values") var_features = sample.get_vector_list("lp_var_features") - var_names = sample.get_vector("static_var_names") + var_names = sample.get_array("static_var_names") var_categories = sample.get_vector("static_var_categories") if var_features is None: var_features = sample.get_vector_list("static_var_features") @@ -187,8 +187,8 @@ class PrimalSolutionComponent(Component): _: Optional[Instance], sample: Sample, ) -> Dict[str, Dict[str, float]]: - mip_var_values = sample.get_vector("mip_var_values") - var_names = sample.get_vector("static_var_names") + mip_var_values = sample.get_array("mip_var_values") + var_names = sample.get_array("static_var_names") assert mip_var_values is not None assert var_names is not None diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 8848e80..cddf462 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -5,7 +5,7 @@ import collections import numbers from math import log, isfinite -from typing import TYPE_CHECKING, Dict, Optional, List, Any, Tuple +from typing import TYPE_CHECKING, Dict, Optional, List, Any, Tuple, KeysView, cast import numpy as np @@ -34,7 +34,7 @@ class FeaturesExtractor: variables = solver.get_variables(with_static=True) constraints = solver.get_constraints(with_static=True, with_lhs=self.with_lhs) sample.put_array("static_var_lower_bounds", variables.lower_bounds) - sample.put_vector("static_var_names", variables.names) + sample.put_array("static_var_names", variables.names) sample.put_array("static_var_obj_coeffs", variables.obj_coeffs) sample.put_vector("static_var_types", variables.types) sample.put_array("static_var_upper_bounds", variables.upper_bounds) @@ -139,12 +139,29 @@ class FeaturesExtractor: instance: "Instance", sample: Sample, ) -> Tuple[List, List]: - categories: List[Optional[str]] = [] - user_features: List[Optional[List[float]]] = [] - var_features_dict = instance.get_variable_features() - var_categories_dict = instance.get_variable_categories() - var_names = sample.get_vector("static_var_names") + # Query variable names + var_names = sample.get_array("static_var_names") assert var_names is not None + + # Query variable features and categories + var_features_dict = { + v.encode(): f for (v, f) in instance.get_variable_features().items() + } + var_categories_dict = { + v.encode(): f for (v, f) in instance.get_variable_categories().items() + } + + # Assert that variables in user-provided dicts actually exist + var_names_set = set(var_names) + for keys in [var_features_dict.keys(), var_categories_dict.keys()]: + for vn in cast(KeysView, keys): + assert ( + vn in var_names_set + ), f"Variable {vn!r} not found in the problem; {var_names_set}" + + # Assemble into compact lists + user_features: List[Optional[List[float]]] = [] + categories: List[Optional[str]] = [] for (i, var_name) in enumerate(var_names): if var_name not in var_categories_dict: user_features.append(None) diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index 724560f..8573717 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -73,11 +73,11 @@ class GurobiSolver(InternalSolver): self._has_lp_solution = False self._has_mip_solution = False - self._varname_to_var: Dict[str, "gurobipy.Var"] = {} + self._varname_to_var: Dict[bytes, "gurobipy.Var"] = {} self._cname_to_constr: Dict[str, "gurobipy.Constr"] = {} self._gp_vars: List["gurobipy.Var"] = [] self._gp_constrs: List["gurobipy.Constr"] = [] - self._var_names: List[str] = [] + self._var_names: np.ndarray = np.empty(0) self._constr_names: List[str] = [] self._var_types: List[str] = [] self._var_lbs: np.ndarray = np.empty(0) @@ -263,11 +263,13 @@ class GurobiSolver(InternalSolver): if self.cb_where is not None: if self.cb_where == self.gp.GRB.Callback.MIPNODE: return { - v.varName: self.model.cbGetNodeRel(v) for v in self.model.getVars() + v.varName.encode(): self.model.cbGetNodeRel(v) + for v in self.model.getVars() } elif self.cb_where == self.gp.GRB.Callback.MIPSOL: return { - v.varName: self.model.cbGetSolution(v) for v in self.model.getVars() + v.varName.encode(): self.model.cbGetSolution(v) + for v in self.model.getVars() } else: raise Exception( @@ -276,7 +278,7 @@ class GurobiSolver(InternalSolver): ) if self.model.solCount == 0: return None - return {v.varName: v.x for v in self.model.getVars()} + return {v.varName.encode(): v.x for v in self.model.getVars()} @overrides def get_variable_attrs(self) -> List[str]: @@ -584,7 +586,10 @@ class GurobiSolver(InternalSolver): assert self.model is not None gp_vars: List["gurobipy.Var"] = self.model.getVars() gp_constrs: List["gurobipy.Constr"] = self.model.getConstrs() - var_names: List[str] = self.model.getAttr("varName", gp_vars) + var_names: np.ndarray = np.array( + self.model.getAttr("varName", gp_vars), + dtype="S", + ) var_types: List[str] = self.model.getAttr("vtype", gp_vars) var_ubs: np.ndarray = np.array( self.model.getAttr("ub", gp_vars), @@ -599,7 +604,7 @@ class GurobiSolver(InternalSolver): dtype=float, ) constr_names: List[str] = self.model.getAttr("constrName", gp_constrs) - varname_to_var: Dict = {} + varname_to_var: Dict[bytes, "gurobipy.Var"] = {} cname_to_constr: Dict = {} for (i, gp_var) in enumerate(gp_vars): assert var_names[i] not in varname_to_var, ( diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index 5754aa9..0fa5ba8 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -50,7 +50,7 @@ class MIPSolveStats: @dataclass class Variables: - names: Optional[List[str]] = None + names: Optional[np.ndarray] = None basis_status: Optional[List[str]] = None lower_bounds: Optional[np.ndarray] = None obj_coeffs: Optional[np.ndarray] = None @@ -71,7 +71,7 @@ class Constraints: basis_status: Optional[List[str]] = None dual_values: Optional[np.ndarray] = None lazy: Optional[List[bool]] = None - lhs: Optional[List[List[Tuple[str, float]]]] = None + lhs: Optional[List[List[Tuple[bytes, float]]]] = None names: Optional[List[str]] = None rhs: Optional[np.ndarray] = None sa_rhs_down: Optional[np.ndarray] = None diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index b3640ec..ad2d541 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -34,8 +34,6 @@ from miplearn.types import ( SolverParams, UserCutCallback, Solution, - VariableName, - Category, ) logger = logging.getLogger(__name__) @@ -59,7 +57,7 @@ class BasePyomoSolver(InternalSolver): self._is_warm_start_available: bool = False self._pyomo_solver: SolverFactory = solver_factory self._obj_sense: str = "min" - self._varname_to_var: Dict[str, pe.Var] = {} + self._varname_to_var: Dict[bytes, pe.Var] = {} self._cname_to_constr: Dict[str, pe.Constraint] = {} self._termination_condition: str = "" self._has_lp_solution = False @@ -166,7 +164,7 @@ class BasePyomoSolver(InternalSolver): names: List[str] = [] rhs: List[float] = [] - lhs: List[List[Tuple[str, float]]] = [] + lhs: List[List[Tuple[bytes, float]]] = [] senses: List[str] = [] dual_values: List[float] = [] slacks: List[float] = [] @@ -199,18 +197,18 @@ class BasePyomoSolver(InternalSolver): if isinstance(term, MonomialTermExpression): lhsc.append( ( - term._args_[1].name, + term._args_[1].name.encode(), float(term._args_[0]), ) ) elif isinstance(term, _GeneralVarData): - lhsc.append((term.name, 1.0)) + lhsc.append((term.name.encode(), 1.0)) else: raise Exception( f"Unknown term type: {term.__class__.__name__}" ) elif isinstance(expr, _GeneralVarData): - lhsc.append((expr.name, 1.0)) + lhsc.append((expr.name.encode(), 1.0)) else: raise Exception( f"Unknown expression type: {expr.__class__.__name__}" @@ -264,7 +262,7 @@ class BasePyomoSolver(InternalSolver): for index in var: if var[index].fixed: continue - solution[f"{var}[{index}]"] = var[index].value + solution[f"{var}[{index}]".encode()] = var[index].value return solution @overrides @@ -328,7 +326,7 @@ class BasePyomoSolver(InternalSolver): values.append(v.value) return Variables( - names=_none_if_empty(names), + names=_none_if_empty(np.array(names, dtype="S")), types=_none_if_empty(types), upper_bounds=_none_if_empty(np.array(upper_bounds, dtype=float)), lower_bounds=_none_if_empty(np.array(lower_bounds, dtype=float)), @@ -558,9 +556,9 @@ class BasePyomoSolver(InternalSolver): self._varname_to_var = {} for var in self.model.component_objects(Var): for idx in var: - varname = f"{var.name}[{idx}]" + varname = f"{var.name}[{idx}]".encode() if idx is None: - varname = var.name + varname = var.name.encode() self._varname_to_var[varname] = var[idx] self._all_vars += [var[idx]] if var[idx].domain == pyomo.core.base.set_types.Binary: diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index c92b07b..5bb070d 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -10,6 +10,7 @@ from miplearn.solvers.internal import InternalSolver, Variables, Constraints inf = float("inf") + # NOTE: # This file is in the main source folder, so that it can be called from Julia. @@ -40,7 +41,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: assert_equals( solver.get_variables(), Variables( - names=["x[0]", "x[1]", "x[2]", "x[3]", "z"], + names=np.array(["x[0]", "x[1]", "x[2]", "x[3]", "z"], dtype="S"), lower_bounds=np.array([0.0, 0.0, 0.0, 0.0, 0.0]), upper_bounds=np.array([1.0, 1.0, 1.0, 1.0, 67.0]), types=["B", "B", "B", "B", "C"], @@ -56,11 +57,11 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: rhs=np.array([0.0]), lhs=[ [ - ("x[0]", 23.0), - ("x[1]", 26.0), - ("x[2]", 20.0), - ("x[3]", 18.0), - ("z", -1.0), + (b"x[0]", 23.0), + (b"x[1]", 26.0), + (b"x[2]", 20.0), + (b"x[3]", 18.0), + (b"z", -1.0), ], ], senses=["="], @@ -83,7 +84,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: _filter_attrs( solver.get_variable_attrs(), Variables( - names=["x[0]", "x[1]", "x[2]", "x[3]", "z"], + names=np.array(["x[0]", "x[1]", "x[2]", "x[3]", "z"], dtype="S"), basis_status=["U", "B", "U", "L", "U"], reduced_costs=np.array( [193.615385, 0.0, 187.230769, -23.692308, 13.538462] @@ -140,7 +141,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: _filter_attrs( solver.get_variable_attrs(), Variables( - names=["x[0]", "x[1]", "x[2]", "x[3]", "z"], + names=np.array(["x[0]", "x[1]", "x[2]", "x[3]", "z"], dtype="S"), values=np.array([1.0, 0.0, 1.0, 1.0, 61.0]), ), ), @@ -161,7 +162,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: # Build new constraint and verify that it is violated cf = Constraints( names=["cut"], - lhs=[[("x[0]", 1.0)]], + lhs=[[(b"x[0]", 1.0)]], rhs=np.array([0.0]), senses=["<"], ) @@ -178,14 +179,14 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: rhs=np.array([0.0, 0.0]), lhs=[ [ - ("x[0]", 23.0), - ("x[1]", 26.0), - ("x[2]", 20.0), - ("x[3]", 18.0), - ("z", -1.0), + (b"x[0]", 23.0), + (b"x[1]", 26.0), + (b"x[2]", 20.0), + (b"x[3]", 18.0), + (b"z", -1.0), ], [ - ("x[0]", 1.0), + (b"x[0]", 1.0), ], ], senses=["=", "<"], @@ -208,16 +209,16 @@ def run_warm_start_tests(solver: InternalSolver) -> None: instance = solver.build_test_instance_knapsack() model = instance.to_model() solver.set_instance(instance, model) - solver.set_warm_start({"x[0]": 1.0, "x[1]": 0.0, "x[2]": 0.0, "x[3]": 1.0}) + solver.set_warm_start({b"x[0]": 1.0, b"x[1]": 0.0, b"x[2]": 0.0, b"x[3]": 1.0}) stats = solver.solve(tee=True) if stats.mip_warm_start_value is not None: assert_equals(stats.mip_warm_start_value, 725.0) - solver.set_warm_start({"x[0]": 1.0, "x[1]": 1.0, "x[2]": 1.0, "x[3]": 1.0}) + solver.set_warm_start({b"x[0]": 1.0, b"x[1]": 1.0, b"x[2]": 1.0, b"x[3]": 1.0}) stats = solver.solve(tee=True) assert stats.mip_warm_start_value is None - solver.fix({"x[0]": 1.0, "x[1]": 0.0, "x[2]": 0.0, "x[3]": 1.0}) + solver.fix({b"x[0]": 1.0, b"x[1]": 0.0, b"x[2]": 0.0, b"x[3]": 1.0}) stats = solver.solve(tee=True) assert_equals(stats.mip_lower_bound, 725.0) assert_equals(stats.mip_upper_bound, 725.0) @@ -257,15 +258,15 @@ def run_lazy_cb_tests(solver: InternalSolver) -> None: def lazy_cb(cb_solver: InternalSolver, cb_model: Any) -> None: relsol = cb_solver.get_solution() assert relsol is not None - assert relsol["x[0]"] is not None - if relsol["x[0]"] > 0: + assert relsol[b"x[0]"] is not None + if relsol[b"x[0]"] > 0: instance.enforce_lazy_constraint(cb_solver, cb_model, "cut") solver.set_instance(instance, model) solver.solve(lazy_cb=lazy_cb) solution = solver.get_solution() assert solution is not None - assert_equals(solution["x[0]"], 0.0) + assert_equals(solution[b"x[0]"], 0.0) def _equals_preprocess(obj: Any) -> Any: @@ -274,7 +275,7 @@ def _equals_preprocess(obj: Any) -> Any: return np.round(obj, decimals=6).tolist() else: return obj.tolist() - elif isinstance(obj, (int, str, bool, np.bool_)): + elif isinstance(obj, (int, str, bool, np.bool_, np.bytes_, bytes)): return obj elif isinstance(obj, float): return round(obj, 6) diff --git a/miplearn/types.py b/miplearn/types.py index 5239c4c..5d94163 100644 --- a/miplearn/types.py +++ b/miplearn/types.py @@ -15,8 +15,7 @@ IterationCallback = Callable[[], bool] LazyCallback = Callable[[Any, Any], None] SolverParams = Dict[str, Any] UserCutCallback = Callable[["InternalSolver", Any], None] -VariableName = str -Solution = Dict[VariableName, Optional[float]] +Solution = Dict[bytes, Optional[float]] LearningSolveStats = TypedDict( "LearningSolveStats", diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index 7672011..0f56d7b 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -22,7 +22,7 @@ from miplearn.solvers.tests import assert_equals def sample() -> Sample: sample = MemorySample( { - "static_var_names": ["x[0]", "x[1]", "x[2]", "x[3]"], + "static_var_names": np.array(["x[0]", "x[1]", "x[2]", "x[3]"], dtype="S"), "static_var_categories": ["default", None, "default", "default"], "mip_var_values": np.array([0.0, 1.0, 1.0, 0.0]), "static_instance_features": [5.0], @@ -112,10 +112,10 @@ def test_usage() -> None: def test_evaluate(sample: Sample) -> None: comp = PrimalSolutionComponent() comp.sample_predict = lambda _: { # type: ignore - "x[0]": 1.0, - "x[1]": 1.0, - "x[2]": 0.0, - "x[3]": None, + b"x[0]": 1.0, + b"x[1]": 1.0, + b"x[2]": 0.0, + b"x[3]": None, } ev = comp.sample_evaluate(None, sample) assert_equals( @@ -150,8 +150,8 @@ def test_predict(sample: Sample) -> None: assert_array_equal(x["default"], clf.predict_proba.call_args[0][0]) assert_array_equal(x["default"], thr.predict.call_args[0][0]) assert pred == { - "x[0]": 0.0, - "x[1]": None, - "x[2]": None, - "x[3]": 1.0, + b"x[0]": 0.0, + b"x[1]": None, + b"x[2]": None, + b"x[3]": 1.0, } diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 2fc1633..bfda285 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -33,7 +33,7 @@ def test_knapsack() -> None: extractor.extract_after_load_features(instance, solver, sample) assert_equals( sample.get_vector("static_var_names"), - ["x[0]", "x[1]", "x[2]", "x[3]", "z"], + np.array(["x[0]", "x[1]", "x[2]", "x[3]", "z"], dtype="S"), ) assert_equals( sample.get_vector("static_var_lower_bounds"), [0.0, 0.0, 0.0, 0.0, 0.0] @@ -126,16 +126,16 @@ def test_constraint_getindex() -> None: senses=["=", "<", ">"], lhs=[ [ - ("x1", 1.0), - ("x2", 1.0), + (b"x1", 1.0), + (b"x2", 1.0), ], [ - ("x2", 2.0), - ("x3", 2.0), + (b"x2", 2.0), + (b"x3", 2.0), ], [ - ("x3", 3.0), - ("x4", 3.0), + (b"x3", 3.0), + (b"x4", 3.0), ], ], ) @@ -147,12 +147,12 @@ def test_constraint_getindex() -> None: senses=["=", ">"], lhs=[ [ - ("x1", 1.0), - ("x2", 1.0), + (b"x1", 1.0), + (b"x2", 1.0), ], [ - ("x3", 3.0), - ("x4", 3.0), + (b"x3", 3.0), + (b"x4", 3.0), ], ], ), From 45667ac2e49056fa4e6c866fd92da0ad1e85147d Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Sun, 8 Aug 2021 07:36:57 -0500 Subject: [PATCH 47/67] Use np.ndarray for var_types, basis_status --- miplearn/features/extractor.py | 4 ++-- miplearn/solvers/gurobi.py | 31 +++++++++++++++++------------- miplearn/solvers/internal.py | 4 ++-- miplearn/solvers/pyomo/base.py | 2 +- miplearn/solvers/tests/__init__.py | 4 ++-- tests/features/test_extractor.py | 9 ++++++--- 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index cddf462..9649991 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -36,7 +36,7 @@ class FeaturesExtractor: sample.put_array("static_var_lower_bounds", variables.lower_bounds) sample.put_array("static_var_names", variables.names) sample.put_array("static_var_obj_coeffs", variables.obj_coeffs) - sample.put_vector("static_var_types", variables.types) + sample.put_array("static_var_types", variables.types) sample.put_array("static_var_upper_bounds", variables.upper_bounds) sample.put_vector("static_constr_names", constraints.names) # sample.put("static_constr_lhs", constraints.lhs) @@ -69,7 +69,7 @@ class FeaturesExtractor: ) -> None: variables = solver.get_variables(with_static=False, with_sa=self.with_sa) constraints = solver.get_constraints(with_static=False, with_sa=self.with_sa) - sample.put_vector("lp_var_basis_status", variables.basis_status) + sample.put_array("lp_var_basis_status", variables.basis_status) sample.put_array("lp_var_reduced_costs", variables.reduced_costs) sample.put_array("lp_var_sa_lb_down", variables.sa_lb_down) sample.put_array("lp_var_sa_lb_up", variables.sa_lb_up) diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index 8573717..1dcadc0 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -79,7 +79,7 @@ class GurobiSolver(InternalSolver): self._gp_constrs: List["gurobipy.Constr"] = [] self._var_names: np.ndarray = np.empty(0) self._constr_names: List[str] = [] - self._var_types: List[str] = [] + self._var_types: np.ndarray = np.empty(0) self._var_lbs: np.ndarray = np.empty(0) self._var_ubs: np.ndarray = np.empty(0) self._var_obj_coeffs: np.ndarray = np.empty(0) @@ -322,8 +322,9 @@ class GurobiSolver(InternalSolver): else: raise Exception(f"unknown vbasis: {basis_status}") + basis_status: Optional[np.ndarray] = None upper_bounds, lower_bounds, types, values = None, None, None, None - obj_coeffs, reduced_costs, basis_status = None, None, None + obj_coeffs, reduced_costs = None, None sa_obj_up, sa_ub_up, sa_lb_up = None, None, None sa_obj_down, sa_ub_down, sa_lb_down = None, None, None @@ -335,11 +336,12 @@ class GurobiSolver(InternalSolver): if self._has_lp_solution: reduced_costs = np.array(model.getAttr("rc", self._gp_vars), dtype=float) - basis_status = list( - map( - _parse_gurobi_vbasis, - model.getAttr("vbasis", self._gp_vars), - ) + basis_status = np.array( + [ + _parse_gurobi_vbasis(b) + for b in model.getAttr("vbasis", self._gp_vars) + ], + dtype="S", ) if with_sa: @@ -513,7 +515,7 @@ class GurobiSolver(InternalSolver): self._apply_params(streams) assert self.model is not None for (i, var) in enumerate(self._gp_vars): - if self._var_types[i] == "B": + if self._var_types[i] == b"B": var.vtype = self.gp.GRB.CONTINUOUS var.lb = 0.0 var.ub = 1.0 @@ -521,7 +523,7 @@ class GurobiSolver(InternalSolver): self.model.optimize() self._dirty = False for (i, var) in enumerate(self._gp_vars): - if self._var_types[i] == "B": + if self._var_types[i] == b"B": var.vtype = self.gp.GRB.BINARY log = streams[0].getvalue() self._has_lp_solution = self.model.solCount > 0 @@ -590,7 +592,10 @@ class GurobiSolver(InternalSolver): self.model.getAttr("varName", gp_vars), dtype="S", ) - var_types: List[str] = self.model.getAttr("vtype", gp_vars) + var_types: np.ndarray = np.array( + self.model.getAttr("vtype", gp_vars), + dtype="S", + ) var_ubs: np.ndarray = np.array( self.model.getAttr("ub", gp_vars), dtype=float, @@ -611,7 +616,7 @@ class GurobiSolver(InternalSolver): f"Duplicated variable name detected: {var_names[i]}. " f"Unique variable names are currently required." ) - if var_types[i] == "I": + if var_types[i] == b"I": assert var_ubs[i] == 1.0, ( "Only binary and continuous variables are currently supported. " f"Integer variable {var_names[i]} has upper bound {var_ubs[i]}." @@ -620,8 +625,8 @@ class GurobiSolver(InternalSolver): "Only binary and continuous variables are currently supported. " f"Integer variable {var_names[i]} has lower bound {var_ubs[i]}." ) - var_types[i] = "B" - assert var_types[i] in ["B", "C"], ( + var_types[i] = b"B" + assert var_types[i] in [b"B", b"C"], ( "Only binary and continuous variables are currently supported. " f"Variable {var_names[i]} has type {var_types[i]}." ) diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index 0fa5ba8..4f48432 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -51,7 +51,7 @@ class MIPSolveStats: @dataclass class Variables: names: Optional[np.ndarray] = None - basis_status: Optional[List[str]] = None + basis_status: Optional[np.ndarray] = None lower_bounds: Optional[np.ndarray] = None obj_coeffs: Optional[np.ndarray] = None reduced_costs: Optional[np.ndarray] = None @@ -61,7 +61,7 @@ class Variables: sa_obj_up: Optional[np.ndarray] = None sa_ub_down: Optional[np.ndarray] = None sa_ub_up: Optional[np.ndarray] = None - types: Optional[List[str]] = None + types: Optional[np.ndarray] = None upper_bounds: Optional[np.ndarray] = None values: Optional[np.ndarray] = None diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index ad2d541..4ccfc17 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -327,7 +327,7 @@ class BasePyomoSolver(InternalSolver): return Variables( names=_none_if_empty(np.array(names, dtype="S")), - types=_none_if_empty(types), + types=_none_if_empty(np.array(types, dtype="S")), upper_bounds=_none_if_empty(np.array(upper_bounds, dtype=float)), lower_bounds=_none_if_empty(np.array(lower_bounds, dtype=float)), obj_coeffs=_none_if_empty(np.array(obj_coeffs, dtype=float)), diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index 5bb070d..7c21d69 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -44,7 +44,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: names=np.array(["x[0]", "x[1]", "x[2]", "x[3]", "z"], dtype="S"), lower_bounds=np.array([0.0, 0.0, 0.0, 0.0, 0.0]), upper_bounds=np.array([1.0, 1.0, 1.0, 1.0, 67.0]), - types=["B", "B", "B", "B", "C"], + types=np.array(["B", "B", "B", "B", "C"], dtype="S"), obj_coeffs=np.array([505.0, 352.0, 458.0, 220.0, 0.0]), ), ) @@ -85,7 +85,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: solver.get_variable_attrs(), Variables( names=np.array(["x[0]", "x[1]", "x[2]", "x[3]", "z"], dtype="S"), - basis_status=["U", "B", "U", "L", "U"], + basis_status=np.array(["U", "B", "U", "L", "U"], dtype="S"), reduced_costs=np.array( [193.615385, 0.0, 187.230769, -23.692308, 13.538462] ), diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index bfda285..c5d448e 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -41,7 +41,10 @@ def test_knapsack() -> None: assert_equals( sample.get_vector("static_var_obj_coeffs"), [505.0, 352.0, 458.0, 220.0, 0.0] ) - assert_equals(sample.get_vector("static_var_types"), ["B", "B", "B", "B", "C"]) + assert_equals( + sample.get_array("static_var_types"), + np.array(["B", "B", "B", "B", "C"], dtype="S"), + ) assert_equals( sample.get_vector("static_var_upper_bounds"), [1.0, 1.0, 1.0, 1.0, 67.0] ) @@ -76,8 +79,8 @@ def test_knapsack() -> None: solver.solve_lp() extractor.extract_after_lp_features(solver, sample) assert_equals( - sample.get_vector("lp_var_basis_status"), - ["U", "B", "U", "L", "U"], + sample.get_array("lp_var_basis_status"), + np.array(["U", "B", "U", "L", "U"], dtype="S"), ) assert_equals( sample.get_vector("lp_var_reduced_costs"), From 9ddda7e1e2774336af6fc8608ba41b8fe1e221f5 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Mon, 9 Aug 2021 05:41:01 -0500 Subject: [PATCH 48/67] Use np.ndarray for constraint names --- miplearn/features/extractor.py | 10 +++++----- miplearn/solvers/gurobi.py | 2 +- miplearn/solvers/internal.py | 8 ++++---- miplearn/solvers/pyomo/base.py | 4 ++-- miplearn/solvers/tests/__init__.py | 12 ++++++------ tests/components/test_static_lazy.py | 10 +++++----- tests/features/test_extractor.py | 14 ++++++++++---- 7 files changed, 33 insertions(+), 27 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 9649991..f4bceaa 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -200,11 +200,11 @@ class FeaturesExtractor: ) -> None: has_static_lazy = instance.has_static_lazy_constraints() user_features: List[Optional[List[float]]] = [] - categories: List[Optional[str]] = [] + categories: List[Optional[bytes]] = [] lazy: List[bool] = [] constr_categories_dict = instance.get_constraint_categories() constr_features_dict = instance.get_constraint_features() - constr_names = sample.get_vector("static_constr_names") + constr_names = sample.get_array("static_constr_names") assert constr_names is not None for (cidx, cname) in enumerate(constr_names): @@ -215,8 +215,8 @@ class FeaturesExtractor: user_features.append(None) categories.append(None) continue - assert isinstance(category, str), ( - f"Constraint category must be a string. " + assert isinstance(category, bytes), ( + f"Constraint category must be bytes. " f"Found {type(category).__name__} instead for cname={cname}.", ) categories.append(category) @@ -242,7 +242,7 @@ class FeaturesExtractor: lazy.append(False) sample.put_vector_list("static_constr_features", user_features) sample.put_vector("static_constr_lazy", lazy) - sample.put_vector("static_constr_categories", categories) + sample.put_array("static_constr_categories", np.array(categories, dtype="S")) def _extract_user_features_instance( self, diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index 1dcadc0..c5705e8 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -211,7 +211,7 @@ class GurobiSolver(InternalSolver): raise Exception(f"unknown cbasis: {v}") gp_constrs = model.getConstrs() - constr_names = model.getAttr("constrName", gp_constrs) + constr_names = np.array(model.getAttr("constrName", gp_constrs), dtype="S") 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 diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index 4f48432..6ea8b2d 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -72,7 +72,7 @@ class Constraints: dual_values: Optional[np.ndarray] = None lazy: Optional[List[bool]] = None lhs: Optional[List[List[Tuple[bytes, float]]]] = None - names: Optional[List[str]] = None + names: Optional[np.ndarray] = None rhs: Optional[np.ndarray] = None sa_rhs_down: Optional[np.ndarray] = None sa_rhs_up: Optional[np.ndarray] = None @@ -86,7 +86,7 @@ class Constraints: dual_values=sample.get_vector("lp_constr_dual_values"), lazy=sample.get_vector("static_constr_lazy"), # lhs=sample.get_vector("static_constr_lhs"), - names=sample.get_vector("static_constr_names"), + names=sample.get_array("static_constr_names"), rhs=sample.get_vector("static_constr_rhs"), sa_rhs_down=sample.get_vector("lp_constr_sa_rhs_down"), sa_rhs_up=sample.get_vector("lp_constr_sa_rhs_up"), @@ -100,7 +100,7 @@ class Constraints: dual_values=( None if self.dual_values is None else self.dual_values[selected] ), - names=self._filter(self.names, selected), + names=(None if self.names is None else self.names[selected]), lazy=self._filter(self.lazy, selected), lhs=self._filter(self.lhs, selected), rhs=(None if self.rhs is None else self.rhs[selected]), @@ -254,7 +254,7 @@ class InternalSolver(ABC): pass @abstractmethod - def remove_constraints(self, names: List[str]) -> None: + def remove_constraints(self, names: np.ndarray) -> None: """ Removes the given constraints from the model. """ diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index 4ccfc17..5e35d57 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -96,7 +96,7 @@ class BasePyomoSolver(InternalSolver): else: expr = lhs >= cf.rhs[i] cl = pe.Constraint(expr=expr, name=name) - self.model.add_component(name, cl) + self.model.add_component(name.decode(), cl) self._pyomo_solver.add_constraint(cl) self._cname_to_constr[name] = cl self._termination_condition = "" @@ -233,7 +233,7 @@ class BasePyomoSolver(InternalSolver): _parse_constraint(constr) return Constraints( - names=_none_if_empty(names), + names=_none_if_empty(np.array(names, dtype="S")), rhs=_none_if_empty(np.array(rhs, dtype=float)), senses=_none_if_empty(senses), lhs=_none_if_empty(lhs), diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index 7c21d69..9524c54 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -53,7 +53,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: assert_equals( solver.get_constraints(), Constraints( - names=["eq_capacity"], + names=np.array(["eq_capacity"], dtype="S"), rhs=np.array([0.0]), lhs=[ [ @@ -110,7 +110,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: Constraints( basis_status=["N"], dual_values=np.array([13.538462]), - names=["eq_capacity"], + names=np.array(["eq_capacity"], dtype="S"), sa_rhs_down=np.array([-24.0]), sa_rhs_up=np.array([2.0]), slacks=np.array([0.0]), @@ -153,7 +153,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: _filter_attrs( solver.get_constraint_attrs(), Constraints( - names=["eq_capacity"], + names=np.array(["eq_capacity"], dtype="S"), slacks=np.array([0.0]), ), ), @@ -161,7 +161,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: # Build new constraint and verify that it is violated cf = Constraints( - names=["cut"], + names=np.array(["cut"], dtype="S"), lhs=[[(b"x[0]", 1.0)]], rhs=np.array([0.0]), senses=["<"], @@ -175,7 +175,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: _filter_attrs( solver.get_constraint_attrs(), Constraints( - names=["eq_capacity", "cut"], + names=np.array(["eq_capacity", "cut"], dtype="S"), rhs=np.array([0.0, 0.0]), lhs=[ [ @@ -198,7 +198,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: assert_equals(solver.are_constraints_satisfied(cf), [True]) # Remove the new constraint - solver.remove_constraints(["cut"]) + solver.remove_constraints(np.array(["cut"], dtype="S")) # New constraint should no longer affect solution stats = solver.solve() diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index 385c7cc..1950786 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -32,9 +32,9 @@ def sample() -> Sample: "type-b", ], "static_constr_lazy": [True, True, True, True, False], - "static_constr_names": ["c1", "c2", "c3", "c4", "c5"], + "static_constr_names": np.array(["c1", "c2", "c3", "c4", "c5"], dtype="S"), "static_instance_features": [5.0], - "mip_constr_lazy_enforced": {"c1", "c2", "c4"}, + "mip_constr_lazy_enforced": {b"c1", b"c2", b"c4"}, "lp_constr_features": [ [1.0, 1.0], [1.0, 2.0], @@ -110,7 +110,7 @@ def test_usage_with_solver(instance: Instance) -> None: # Should ask internal solver to remove some constraints assert internal.remove_constraints.call_count == 1 - internal.remove_constraints.assert_has_calls([call(["c3"])]) + internal.remove_constraints.assert_has_calls([call([b"c3"])]) # LearningSolver calls after_iteration (first time) should_repeat = component.iteration_cb(solver, instance, None) @@ -142,7 +142,7 @@ def test_usage_with_solver(instance: Instance) -> None: ) # Should update training sample - assert sample.get_set("mip_constr_lazy_enforced") == {"c1", "c2", "c3", "c4"} + assert sample.get_set("mip_constr_lazy_enforced") == {b"c1", b"c2", b"c3", b"c4"} # # Should update stats assert stats["LazyStatic: Removed"] == 1 @@ -170,7 +170,7 @@ def test_sample_predict(sample: Sample) -> None: ] ) pred = comp.sample_predict(sample) - assert pred == ["c1", "c2", "c4"] + assert pred == [b"c1", b"c2", b"c4"] def test_fit_xy() -> None: diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index c5d448e..3203aaf 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -53,7 +53,10 @@ def test_knapsack() -> None: ["default", "default", "default", "default", None], ) assert sample.get_vector_list("static_var_features") is not None - assert_equals(sample.get_vector("static_constr_names"), ["eq_capacity"]) + assert_equals( + sample.get_vector("static_constr_names"), + np.array(["eq_capacity"], dtype="S"), + ) # assert_equals( # sample.get_vector("static_constr_lhs"), # [ @@ -69,7 +72,10 @@ def test_knapsack() -> None: assert_equals(sample.get_vector("static_constr_rhs"), [0.0]) assert_equals(sample.get_vector("static_constr_senses"), ["="]) assert_equals(sample.get_vector("static_constr_features"), [None]) - assert_equals(sample.get_vector("static_constr_categories"), ["eq_capacity"]) + assert_equals( + sample.get_vector("static_constr_categories"), + np.array(["eq_capacity"], dtype="S"), + ) assert_equals(sample.get_vector("static_constr_lazy"), [False]) assert_equals(sample.get_vector("static_instance_features"), [67.0, 21.75]) assert_equals(sample.get_scalar("static_constr_lazy_count"), 0) @@ -124,7 +130,7 @@ def test_knapsack() -> None: def test_constraint_getindex() -> None: cf = Constraints( - names=["c1", "c2", "c3"], + names=np.array(["c1", "c2", "c3"], dtype="S"), rhs=np.array([1.0, 2.0, 3.0]), senses=["=", "<", ">"], lhs=[ @@ -145,7 +151,7 @@ def test_constraint_getindex() -> None: assert_equals( cf[[True, False, True]], Constraints( - names=["c1", "c3"], + names=np.array(["c1", "c3"], dtype="S"), rhs=np.array([1.0, 3.0]), senses=["=", ">"], lhs=[ From f809dd7de4b4562bb6272a04fe11087be8bce224 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Mon, 9 Aug 2021 06:04:14 -0500 Subject: [PATCH 49/67] Use np.ndarray in Constraints.{basis_status,senses} --- miplearn/components/static_lazy.py | 2 +- miplearn/features/extractor.py | 6 +++--- miplearn/solvers/gurobi.py | 18 +++++++++--------- miplearn/solvers/internal.py | 14 ++++++++------ miplearn/solvers/pyomo/base.py | 10 ++++++---- miplearn/solvers/tests/__init__.py | 8 ++++---- tests/features/test_extractor.py | 16 +++++++++++----- 7 files changed, 42 insertions(+), 32 deletions(-) diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 53a7a7d..6db8be0 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -206,7 +206,7 @@ class StaticLazyConstraintsComponent(Component): cids: Dict[str, List[str]] = {} instance_features = sample.get_vector("static_instance_features") constr_features = sample.get_vector_list("lp_constr_features") - constr_names = sample.get_vector("static_constr_names") + constr_names = sample.get_array("static_constr_names") constr_categories = sample.get_vector("static_constr_categories") constr_lazy = sample.get_vector("static_constr_lazy") lazy_enforced = sample.get_set("mip_constr_lazy_enforced") diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index f4bceaa..8f3ca24 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -38,10 +38,10 @@ class FeaturesExtractor: sample.put_array("static_var_obj_coeffs", variables.obj_coeffs) sample.put_array("static_var_types", variables.types) sample.put_array("static_var_upper_bounds", variables.upper_bounds) - sample.put_vector("static_constr_names", constraints.names) + sample.put_array("static_constr_names", constraints.names) # sample.put("static_constr_lhs", constraints.lhs) sample.put_array("static_constr_rhs", constraints.rhs) - sample.put_vector("static_constr_senses", constraints.senses) + sample.put_array("static_constr_senses", constraints.senses) vars_features_user, var_categories = self._extract_user_features_vars( instance, sample ) @@ -78,7 +78,7 @@ class FeaturesExtractor: sample.put_array("lp_var_sa_ub_down", variables.sa_ub_down) sample.put_array("lp_var_sa_ub_up", variables.sa_ub_up) sample.put_array("lp_var_values", variables.values) - sample.put_vector("lp_constr_basis_status", constraints.basis_status) + sample.put_array("lp_constr_basis_status", constraints.basis_status) sample.put_array("lp_constr_dual_values", constraints.dual_values) sample.put_array("lp_constr_sa_rhs_down", constraints.sa_rhs_down) sample.put_array("lp_constr_sa_rhs_up", constraints.sa_rhs_up) diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index c5705e8..536750e 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -104,12 +104,14 @@ class GurobiSolver(InternalSolver): lhs = self.gp.quicksum( self._varname_to_var[varname] * coeff for (varname, coeff) in cf.lhs[i] ) - if sense == "=": + if sense == b"=": self.model.addConstr(lhs == cf.rhs[i], name=cf.names[i]) - elif sense == "<": + elif sense == b"<": self.model.addConstr(lhs <= cf.rhs[i], name=cf.names[i]) - else: + elif sense == b">": self.model.addConstr(lhs >= cf.rhs[i], name=cf.names[i]) + else: + raise Exception(f"Unknown sense: {sense}") self.model.update() self._dirty = True self._has_lp_solution = False @@ -218,7 +220,7 @@ class GurobiSolver(InternalSolver): if with_static: rhs = np.array(model.getAttr("rhs", gp_constrs), dtype=float) - senses = model.getAttr("sense", gp_constrs) + senses = np.array(model.getAttr("sense", gp_constrs), dtype="S") if with_lhs: lhs = [None for _ in gp_constrs] for (i, gp_constr) in enumerate(gp_constrs): @@ -230,11 +232,9 @@ class GurobiSolver(InternalSolver): if self._has_lp_solution: dual_value = np.array(model.getAttr("pi", gp_constrs), dtype=float) - basis_status = list( - map( - _parse_gurobi_cbasis, - model.getAttr("cbasis", gp_constrs), - ) + basis_status = np.array( + [_parse_gurobi_cbasis(c) for c in model.getAttr("cbasis", gp_constrs)], + dtype="S", ) if with_sa: sa_rhs_up = np.array(model.getAttr("saRhsUp", gp_constrs), dtype=float) diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index 6ea8b2d..ba45683 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -68,7 +68,7 @@ class Variables: @dataclass class Constraints: - basis_status: Optional[List[str]] = None + basis_status: Optional[np.ndarray] = None dual_values: Optional[np.ndarray] = None lazy: Optional[List[bool]] = None lhs: Optional[List[List[Tuple[bytes, float]]]] = None @@ -76,13 +76,13 @@ class Constraints: rhs: Optional[np.ndarray] = None sa_rhs_down: Optional[np.ndarray] = None sa_rhs_up: Optional[np.ndarray] = None - senses: Optional[List[str]] = None + senses: Optional[np.ndarray] = None slacks: Optional[np.ndarray] = None @staticmethod def from_sample(sample: "Sample") -> "Constraints": return Constraints( - basis_status=sample.get_vector("lp_constr_basis_status"), + basis_status=sample.get_array("lp_constr_basis_status"), dual_values=sample.get_vector("lp_constr_dual_values"), lazy=sample.get_vector("static_constr_lazy"), # lhs=sample.get_vector("static_constr_lhs"), @@ -90,13 +90,15 @@ class Constraints: rhs=sample.get_vector("static_constr_rhs"), sa_rhs_down=sample.get_vector("lp_constr_sa_rhs_down"), sa_rhs_up=sample.get_vector("lp_constr_sa_rhs_up"), - senses=sample.get_vector("static_constr_senses"), + senses=sample.get_array("static_constr_senses"), slacks=sample.get_vector("lp_constr_slacks"), ) def __getitem__(self, selected: List[bool]) -> "Constraints": return Constraints( - basis_status=self._filter(self.basis_status, selected), + basis_status=( + None if self.basis_status is None else self.basis_status[selected] + ), dual_values=( None if self.dual_values is None else self.dual_values[selected] ), @@ -108,7 +110,7 @@ class Constraints: None if self.sa_rhs_down is None else self.sa_rhs_down[selected] ), sa_rhs_up=(None if self.sa_rhs_up is None else self.sa_rhs_up[selected]), - senses=self._filter(self.senses, selected), + senses=(None if self.senses is None else self.senses[selected]), slacks=(None if self.slacks is None else self.slacks[selected]), ) diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index 5e35d57..ed0ba59 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -89,12 +89,14 @@ class BasePyomoSolver(InternalSolver): for (varname, coeff) in cf.lhs[i]: var = self._varname_to_var[varname] lhs += var * coeff - if cf.senses[i] == "=": + if cf.senses[i] == b"=": expr = lhs == cf.rhs[i] - elif cf.senses[i] == "<": + elif cf.senses[i] == b"<": expr = lhs <= cf.rhs[i] - else: + elif cf.senses[i] == b">": expr = lhs >= cf.rhs[i] + else: + raise Exception(f"Unknown sense: {cf.senses[i]}") cl = pe.Constraint(expr=expr, name=name) self.model.add_component(name.decode(), cl) self._pyomo_solver.add_constraint(cl) @@ -235,7 +237,7 @@ class BasePyomoSolver(InternalSolver): return Constraints( names=_none_if_empty(np.array(names, dtype="S")), rhs=_none_if_empty(np.array(rhs, dtype=float)), - senses=_none_if_empty(senses), + senses=_none_if_empty(np.array(senses, dtype="S")), lhs=_none_if_empty(lhs), slacks=_none_if_empty(np.array(slacks, dtype=float)), dual_values=_none_if_empty(np.array(dual_values, dtype=float)), diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index 9524c54..8caaffb 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -64,7 +64,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: (b"z", -1.0), ], ], - senses=["="], + senses=np.array(["="], dtype="S"), ), ) @@ -108,7 +108,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: _filter_attrs( solver.get_constraint_attrs(), Constraints( - basis_status=["N"], + basis_status=np.array(["N"], dtype="S"), dual_values=np.array([13.538462]), names=np.array(["eq_capacity"], dtype="S"), sa_rhs_down=np.array([-24.0]), @@ -164,7 +164,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: names=np.array(["cut"], dtype="S"), lhs=[[(b"x[0]", 1.0)]], rhs=np.array([0.0]), - senses=["<"], + senses=np.array(["<"], dtype="S"), ) assert_equals(solver.are_constraints_satisfied(cf), [False]) @@ -189,7 +189,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: (b"x[0]", 1.0), ], ], - senses=["=", "<"], + senses=np.array(["=", "<"], dtype="S"), ), ), ) diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 3203aaf..a1197e3 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -54,7 +54,7 @@ def test_knapsack() -> None: ) assert sample.get_vector_list("static_var_features") is not None assert_equals( - sample.get_vector("static_constr_names"), + sample.get_array("static_constr_names"), np.array(["eq_capacity"], dtype="S"), ) # assert_equals( @@ -70,7 +70,10 @@ def test_knapsack() -> None: # ], # ) assert_equals(sample.get_vector("static_constr_rhs"), [0.0]) - assert_equals(sample.get_vector("static_constr_senses"), ["="]) + assert_equals( + sample.get_array("static_constr_senses"), + np.array(["="], dtype="S"), + ) assert_equals(sample.get_vector("static_constr_features"), [None]) assert_equals( sample.get_vector("static_constr_categories"), @@ -114,7 +117,10 @@ def test_knapsack() -> None: assert_equals(sample.get_vector("lp_var_sa_ub_up"), [2.043478, inf, 2.2, inf, 69.0]) assert_equals(sample.get_vector("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0]) assert sample.get_vector_list("lp_var_features") is not None - assert_equals(sample.get_vector("lp_constr_basis_status"), ["N"]) + assert_equals( + sample.get_array("lp_constr_basis_status"), + np.array(["N"], dtype="S"), + ) assert_equals(sample.get_vector("lp_constr_dual_values"), [13.538462]) assert_equals(sample.get_vector("lp_constr_sa_rhs_down"), [-24.0]) assert_equals(sample.get_vector("lp_constr_sa_rhs_up"), [2.0]) @@ -132,7 +138,7 @@ def test_constraint_getindex() -> None: cf = Constraints( names=np.array(["c1", "c2", "c3"], dtype="S"), rhs=np.array([1.0, 2.0, 3.0]), - senses=["=", "<", ">"], + senses=np.array(["=", "<", ">"], dtype="S"), lhs=[ [ (b"x1", 1.0), @@ -153,7 +159,7 @@ def test_constraint_getindex() -> None: Constraints( names=np.array(["c1", "c3"], dtype="S"), rhs=np.array([1.0, 3.0]), - senses=["=", ">"], + senses=np.array(["=", ">"], dtype="S"), lhs=[ [ (b"x1", 1.0), From 5b54153a3a8b30121b6c56d617abf2827c309315 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Mon, 9 Aug 2021 06:27:03 -0500 Subject: [PATCH 50/67] Use np in Constraints.lazy; replace some get_vector --- miplearn/components/primal.py | 2 +- miplearn/components/static_lazy.py | 4 ++-- miplearn/features/extractor.py | 14 ++++++------ miplearn/features/sample.py | 12 +++++----- miplearn/solvers/internal.py | 16 +++++++------- tests/components/test_static_lazy.py | 2 +- tests/features/test_extractor.py | 32 +++++++++++++-------------- tests/instance/test_file.py | 4 ++-- tests/problems/test_tsp.py | 4 ++-- tests/solvers/test_learning_solver.py | 4 ++-- 10 files changed, 47 insertions(+), 47 deletions(-) diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index c08d5cb..2e40623 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -143,7 +143,7 @@ class PrimalSolutionComponent(Component): x: Dict = {} y: Dict = {} instance_features = sample.get_vector("static_instance_features") - mip_var_values = sample.get_vector("mip_var_values") + mip_var_values = sample.get_array("mip_var_values") var_features = sample.get_vector_list("lp_var_features") var_names = sample.get_array("static_var_names") var_categories = sample.get_vector("static_var_categories") diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 6db8be0..8a85027 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -183,7 +183,7 @@ class StaticLazyConstraintsComponent(Component): logger.info(f"Found {n_violated} violated lazy constraints found") if n_violated > 0: logger.info( - "Enforcing {n_violated} lazy constraints; " + f"Enforcing {n_violated} lazy constraints; " f"{n_satisfied} left in the pool..." ) solver.internal_solver.add_constraints(violated_constraints) @@ -208,7 +208,7 @@ class StaticLazyConstraintsComponent(Component): constr_features = sample.get_vector_list("lp_constr_features") constr_names = sample.get_array("static_constr_names") constr_categories = sample.get_vector("static_constr_categories") - constr_lazy = sample.get_vector("static_constr_lazy") + constr_lazy = sample.get_array("static_constr_lazy") lazy_enforced = sample.get_set("mip_constr_lazy_enforced") if constr_features is None: constr_features = sample.get_vector_list("static_constr_features") diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 8f3ca24..6518309 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -241,7 +241,7 @@ class FeaturesExtractor: else: lazy.append(False) sample.put_vector_list("static_constr_features", user_features) - sample.put_vector("static_constr_lazy", lazy) + sample.put_array("static_constr_lazy", np.array(lazy, dtype=bool)) sample.put_array("static_constr_categories", np.array(categories, dtype="S")) def _extract_user_features_instance( @@ -261,18 +261,18 @@ class FeaturesExtractor: f"Instance features must be a list of numbers. " f"Found {type(v).__name__} instead." ) - constr_lazy = sample.get_vector("static_constr_lazy") + constr_lazy = sample.get_array("static_constr_lazy") assert constr_lazy is not None sample.put_vector("static_instance_features", user_features) - sample.put_scalar("static_constr_lazy_count", sum(constr_lazy)) + sample.put_scalar("static_constr_lazy_count", int(sum(constr_lazy))) # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based # approximation of strong branching. INFORMS Journal on Computing, 29(1), 185-195. def _extract_var_features_AlvLouWeh2017(self, sample: Sample) -> List: - obj_coeffs = sample.get_vector("static_var_obj_coeffs") - obj_sa_down = sample.get_vector("lp_var_sa_obj_down") - obj_sa_up = sample.get_vector("lp_var_sa_obj_up") - values = sample.get_vector(f"lp_var_values") + obj_coeffs = sample.get_array("static_var_obj_coeffs") + obj_sa_down = sample.get_array("lp_var_sa_obj_down") + obj_sa_up = sample.get_array("lp_var_sa_obj_up") + values = sample.get_array("lp_var_values") assert obj_coeffs is not None pos_obj_coeff_sum = 0.0 diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 10ff3e5..fa0085b 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -94,21 +94,21 @@ class Sample(ABC): def _assert_is_scalar(self, value: Any) -> None: if value is None: return - if isinstance(value, (str, bool, int, float)): + if isinstance(value, (str, bool, int, float, np.bytes_)): return - assert False, f"scalar expected; found instead: {value}" + assert False, f"scalar expected; found instead: {value} ({value.__class__})" def _assert_is_vector(self, value: Any) -> None: assert isinstance( value, (list, np.ndarray) - ), f"list or numpy array expected; found instead: {value}" + ), f"list or numpy array expected; found instead: {value} ({value.__class__})" for v in value: self._assert_is_scalar(v) def _assert_is_vector_list(self, value: Any) -> None: assert isinstance( value, (list, np.ndarray) - ), f"list or numpy array expected; found instead: {value}" + ), f"list or numpy array expected; found instead: {value} ({value.__class__})" for v in value: if v is None: continue @@ -125,7 +125,7 @@ class MemorySample(Sample): def __init__( self, data: Optional[Dict[str, Any]] = None, - check_data: bool = False, + check_data: bool = True, ) -> None: if data is None: data = {} @@ -210,7 +210,7 @@ class Hdf5Sample(Sample): self, filename: str, mode: str = "r+", - check_data: bool = False, + check_data: bool = True, ) -> None: self.file = h5py.File(filename, mode, libver="latest") self._check_data = check_data diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index ba45683..1865159 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -70,7 +70,7 @@ class Variables: class Constraints: basis_status: Optional[np.ndarray] = None dual_values: Optional[np.ndarray] = None - lazy: Optional[List[bool]] = None + lazy: Optional[np.ndarray] = None lhs: Optional[List[List[Tuple[bytes, float]]]] = None names: Optional[np.ndarray] = None rhs: Optional[np.ndarray] = None @@ -83,15 +83,15 @@ class Constraints: def from_sample(sample: "Sample") -> "Constraints": return Constraints( basis_status=sample.get_array("lp_constr_basis_status"), - dual_values=sample.get_vector("lp_constr_dual_values"), - lazy=sample.get_vector("static_constr_lazy"), + dual_values=sample.get_array("lp_constr_dual_values"), + lazy=sample.get_array("static_constr_lazy"), # lhs=sample.get_vector("static_constr_lhs"), names=sample.get_array("static_constr_names"), - rhs=sample.get_vector("static_constr_rhs"), - sa_rhs_down=sample.get_vector("lp_constr_sa_rhs_down"), - sa_rhs_up=sample.get_vector("lp_constr_sa_rhs_up"), + rhs=sample.get_array("static_constr_rhs"), + sa_rhs_down=sample.get_array("lp_constr_sa_rhs_down"), + sa_rhs_up=sample.get_array("lp_constr_sa_rhs_up"), senses=sample.get_array("static_constr_senses"), - slacks=sample.get_vector("lp_constr_slacks"), + slacks=sample.get_array("lp_constr_slacks"), ) def __getitem__(self, selected: List[bool]) -> "Constraints": @@ -103,7 +103,7 @@ class Constraints: None if self.dual_values is None else self.dual_values[selected] ), names=(None if self.names is None else self.names[selected]), - lazy=self._filter(self.lazy, selected), + lazy=(None if self.lazy is None else self.lazy[selected]), lhs=self._filter(self.lhs, selected), rhs=(None if self.rhs is None else self.rhs[selected]), sa_rhs_down=( diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index 1950786..8220244 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -31,7 +31,7 @@ def sample() -> Sample: "type-b", "type-b", ], - "static_constr_lazy": [True, True, True, True, False], + "static_constr_lazy": np.array([True, True, True, True, False]), "static_constr_names": np.array(["c1", "c2", "c3", "c4", "c5"], dtype="S"), "static_instance_features": [5.0], "mip_constr_lazy_enforced": {b"c1", b"c2", b"c4"}, diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index a1197e3..25bbf7d 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -39,7 +39,7 @@ def test_knapsack() -> None: sample.get_vector("static_var_lower_bounds"), [0.0, 0.0, 0.0, 0.0, 0.0] ) assert_equals( - sample.get_vector("static_var_obj_coeffs"), [505.0, 352.0, 458.0, 220.0, 0.0] + sample.get_array("static_var_obj_coeffs"), [505.0, 352.0, 458.0, 220.0, 0.0] ) assert_equals( sample.get_array("static_var_types"), @@ -79,7 +79,7 @@ def test_knapsack() -> None: sample.get_vector("static_constr_categories"), np.array(["eq_capacity"], dtype="S"), ) - assert_equals(sample.get_vector("static_constr_lazy"), [False]) + assert_equals(sample.get_array("static_constr_lazy"), np.array([False])) assert_equals(sample.get_vector("static_instance_features"), [67.0, 21.75]) assert_equals(sample.get_scalar("static_constr_lazy_count"), 0) @@ -92,46 +92,46 @@ def test_knapsack() -> None: np.array(["U", "B", "U", "L", "U"], dtype="S"), ) assert_equals( - sample.get_vector("lp_var_reduced_costs"), + sample.get_array("lp_var_reduced_costs"), [193.615385, 0.0, 187.230769, -23.692308, 13.538462], ) assert_equals( - sample.get_vector("lp_var_sa_lb_down"), + sample.get_array("lp_var_sa_lb_down"), [-inf, -inf, -inf, -0.111111, -inf], ) assert_equals( - sample.get_vector("lp_var_sa_lb_up"), + sample.get_array("lp_var_sa_lb_up"), [1.0, 0.923077, 1.0, 1.0, 67.0], ) assert_equals( - sample.get_vector("lp_var_sa_obj_down"), + sample.get_array("lp_var_sa_obj_down"), [311.384615, 317.777778, 270.769231, -inf, -13.538462], ) assert_equals( - sample.get_vector("lp_var_sa_obj_up"), + sample.get_array("lp_var_sa_obj_up"), [inf, 570.869565, inf, 243.692308, inf], ) assert_equals( - sample.get_vector("lp_var_sa_ub_down"), [0.913043, 0.923077, 0.9, 0.0, 43.0] + sample.get_array("lp_var_sa_ub_down"), [0.913043, 0.923077, 0.9, 0.0, 43.0] ) - assert_equals(sample.get_vector("lp_var_sa_ub_up"), [2.043478, inf, 2.2, inf, 69.0]) - assert_equals(sample.get_vector("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0]) + assert_equals(sample.get_array("lp_var_sa_ub_up"), [2.043478, inf, 2.2, inf, 69.0]) + assert_equals(sample.get_array("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0]) assert sample.get_vector_list("lp_var_features") is not None assert_equals( sample.get_array("lp_constr_basis_status"), np.array(["N"], dtype="S"), ) - assert_equals(sample.get_vector("lp_constr_dual_values"), [13.538462]) - assert_equals(sample.get_vector("lp_constr_sa_rhs_down"), [-24.0]) - assert_equals(sample.get_vector("lp_constr_sa_rhs_up"), [2.0]) - assert_equals(sample.get_vector("lp_constr_slacks"), [0.0]) + assert_equals(sample.get_array("lp_constr_dual_values"), [13.538462]) + assert_equals(sample.get_array("lp_constr_sa_rhs_down"), [-24.0]) + assert_equals(sample.get_array("lp_constr_sa_rhs_up"), [2.0]) + assert_equals(sample.get_array("lp_constr_slacks"), [0.0]) # after-mip # ------------------------------------------------------- solver.solve() extractor.extract_after_mip_features(solver, sample) - assert_equals(sample.get_vector("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 61.0]) - assert_equals(sample.get_vector("mip_constr_slacks"), [0.0]) + assert_equals(sample.get_array("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 61.0]) + assert_equals(sample.get_array("mip_constr_slacks"), [0.0]) def test_constraint_getindex() -> None: diff --git a/tests/instance/test_file.py b/tests/instance/test_file.py index 5cf4d22..4dfb607 100644 --- a/tests/instance/test_file.py +++ b/tests/instance/test_file.py @@ -28,5 +28,5 @@ def test_usage() -> None: sample = FileInstance(filename).get_samples()[0] assert sample.get_scalar("mip_lower_bound") == 1183.0 assert sample.get_scalar("mip_upper_bound") == 1183.0 - assert len(sample.get_vector("lp_var_values")) == 5 - assert len(sample.get_vector("mip_var_values")) == 5 + assert len(sample.get_array("lp_var_values")) == 5 + assert len(sample.get_array("mip_var_values")) == 5 diff --git a/tests/problems/test_tsp.py b/tests/problems/test_tsp.py index 4a1079b..99a4be6 100644 --- a/tests/problems/test_tsp.py +++ b/tests/problems/test_tsp.py @@ -42,7 +42,7 @@ def test_instance() -> None: solver.solve(instance) assert len(instance.get_samples()) == 1 sample = instance.get_samples()[0] - assert_equals(sample.get_vector("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 0.0, 1.0]) + assert_equals(sample.get_array("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 0.0, 1.0]) assert sample.get_scalar("mip_lower_bound") == 4.0 assert sample.get_scalar("mip_upper_bound") == 4.0 @@ -70,7 +70,7 @@ def test_subtour() -> None: assert lazy_enforced is not None assert len(lazy_enforced) > 0 assert_equals( - sample.get_vector("mip_var_values"), + sample.get_array("mip_var_values"), [ 1.0, 0.0, diff --git a/tests/solvers/test_learning_solver.py b/tests/solvers/test_learning_solver.py index bc4db78..97fcf47 100644 --- a/tests/solvers/test_learning_solver.py +++ b/tests/solvers/test_learning_solver.py @@ -39,7 +39,7 @@ def test_learning_solver( sample = instance.get_samples()[0] assert_equals( - sample.get_vector("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 61.0] + sample.get_array("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 61.0] ) assert sample.get_scalar("mip_lower_bound") == 1183.0 assert sample.get_scalar("mip_upper_bound") == 1183.0 @@ -48,7 +48,7 @@ def test_learning_solver( assert len(mip_log) > 100 assert_equals( - sample.get_vector("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0] + sample.get_array("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0] ) assert_equals(sample.get_scalar("lp_value"), 1287.923077) lp_log = sample.get_scalar("lp_log") From 63eff336e207b4269a1c72204e8d2653588100f2 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Mon, 9 Aug 2021 07:09:02 -0500 Subject: [PATCH 51/67] Implement sample.{get,put}_sparse --- miplearn/features/sample.py | 44 ++++++++++++++++++++++++++++ tests/features/test_sample.py | 54 +++++++++++++++++++++++++---------- 2 files changed, 83 insertions(+), 15 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index fa0085b..4842bc5 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -5,6 +5,7 @@ import warnings from abc import ABC, abstractmethod from copy import deepcopy from typing import Dict, Optional, Any, Union, List, Tuple, cast, Set +from scipy.sparse import coo_matrix import h5py import numpy as np @@ -80,6 +81,14 @@ class Sample(ABC): def get_array(self, key: str) -> Optional[np.ndarray]: pass + @abstractmethod + def put_sparse(self, key: str, value: coo_matrix) -> None: + pass + + @abstractmethod + def get_sparse(self, key: str) -> Optional[coo_matrix]: + pass + def get_set(self, key: str) -> Set: v = self.get_vector(key) if v: @@ -118,6 +127,10 @@ class Sample(ABC): assert isinstance(value, np.ndarray) assert value.dtype.kind in "biufS", f"Unsupported dtype: {value.dtype}" + def _assert_is_sparse(self, value: Any) -> None: + assert isinstance(value, coo_matrix) + self._assert_supported(value.data) + class MemorySample(Sample): """Dictionary-like class that stores training data in-memory.""" @@ -197,6 +210,17 @@ class MemorySample(Sample): def get_array(self, key: str) -> Optional[np.ndarray]: return cast(Optional[np.ndarray], self._get(key)) + @overrides + def put_sparse(self, key: str, value: coo_matrix) -> None: + if value is None: + return + self._assert_is_sparse(value) + self._put(key, value) + + @overrides + def get_sparse(self, key: str) -> Optional[coo_matrix]: + return cast(Optional[coo_matrix], self._get(key)) + class Hdf5Sample(Sample): """ @@ -351,6 +375,26 @@ class Hdf5Sample(Sample): return None return self.file[key][:] + @overrides + def put_sparse(self, key: str, value: coo_matrix) -> None: + if value is None: + return + self._assert_is_sparse(value) + self.put_array(f"{key}_row", value.row) + self.put_array(f"{key}_col", value.col) + self.put_array(f"{key}_data", value.data) + + @overrides + def get_sparse(self, key: str) -> Optional[coo_matrix]: + row = self.get_array(f"{key}_row") + if row is None: + return None + col = self.get_array(f"{key}_col") + data = self.get_array(f"{key}_data") + assert col is not None + assert data is not None + return coo_matrix((data, (row, col))) + def _pad(veclist: VectorList) -> Tuple[VectorList, List[int]]: veclist = deepcopy(veclist) diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index 9727713..6802848 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -5,6 +5,7 @@ from tempfile import NamedTemporaryFile from typing import Any import numpy as np +from scipy.sparse import coo_matrix from miplearn.features.sample import MemorySample, Sample, Hdf5Sample @@ -23,6 +24,8 @@ def _test_sample(sample: Sample) -> None: _assert_roundtrip_scalar(sample, True) _assert_roundtrip_scalar(sample, 1) _assert_roundtrip_scalar(sample, 1.0) + assert sample.get_scalar("unknown-key") is None + _assert_roundtrip_array(sample, np.array([True, False], dtype="bool")) _assert_roundtrip_array(sample, np.array([1, 2, 3], dtype="int16")) _assert_roundtrip_array(sample, np.array([1, 2, 3], dtype="int32")) @@ -31,24 +34,45 @@ def _test_sample(sample: Sample) -> None: _assert_roundtrip_array(sample, np.array([1.0, 2.0, 3.0], dtype="float32")) _assert_roundtrip_array(sample, np.array([1.0, 2.0, 3.0], dtype="float64")) _assert_roundtrip_array(sample, np.array(["A", "BB", "CCC"], dtype="S")) - assert sample.get_scalar("unknown-key") is None assert sample.get_array("unknown-key") is None + _assert_roundtrip_sparse( + sample, + coo_matrix( + [ + [1, 0, 0], + [0, 2, 3], + [0, 0, 4], + ], + dtype=float, + ), + ) + assert sample.get_sparse("unknown-key") is None -def _assert_roundtrip_array(sample: Sample, expected: Any) -> None: - sample.put_array("key", expected) - actual = sample.get_array("key") - assert actual is not None - assert isinstance(actual, np.ndarray) - assert actual.dtype == expected.dtype - assert (actual == expected).all() +def _assert_roundtrip_array(sample: Sample, original: np.ndarray) -> None: + sample.put_array("key", original) + recovered = sample.get_array("key") + assert recovered is not None + assert isinstance(recovered, np.ndarray) + assert recovered.dtype == original.dtype + assert (recovered == original).all() -def _assert_roundtrip_scalar(sample: Sample, expected: Any) -> None: - sample.put_scalar("key", expected) - actual = sample.get_scalar("key") - assert actual == expected - assert actual is not None + +def _assert_roundtrip_scalar(sample: Sample, original: Any) -> None: + sample.put_scalar("key", original) + recovered = sample.get_scalar("key") + assert recovered == original + assert recovered is not None assert isinstance( - actual, expected.__class__ - ), f"Expected {expected.__class__}, found {actual.__class__} instead" + recovered, original.__class__ + ), f"Expected {original.__class__}, found {recovered.__class__} instead" + + +def _assert_roundtrip_sparse(sample: Sample, original: coo_matrix) -> None: + sample.put_sparse("key", original) + recovered = sample.get_sparse("key") + assert recovered is not None + assert isinstance(recovered, coo_matrix) + assert recovered.dtype == original.dtype + assert (original != recovered).sum() == 0 From 47d30118088472a1b7d21ee5d9f9921188961870 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Mon, 9 Aug 2021 10:01:58 -0500 Subject: [PATCH 52/67] Use np.ndarray in instance features --- miplearn/components/dynamic_common.py | 2 +- miplearn/features/extractor.py | 50 ++++++++++++++------------- miplearn/solvers/learning.py | 9 +++-- tests/features/test_extractor.py | 8 ++--- 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/miplearn/components/dynamic_common.py b/miplearn/components/dynamic_common.py index ce78dd2..bc28438 100644 --- a/miplearn/components/dynamic_common.py +++ b/miplearn/components/dynamic_common.py @@ -52,7 +52,7 @@ class DynamicConstraintsComponent(Component): cids: Dict[str, List[str]] = {} constr_categories_dict = instance.get_constraint_categories() constr_features_dict = instance.get_constraint_features() - instance_features = sample.get_vector("static_instance_features") + instance_features = sample.get_array("static_instance_features") assert instance_features is not None for cid in self.known_cids: # Initialize categories diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 6518309..184694a 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Dict, Optional, List, Any, Tuple, KeysView, ca import numpy as np from miplearn.features.sample import Sample +from miplearn.solvers.internal import LPSolveStats if TYPE_CHECKING: from miplearn.solvers.internal import InternalSolver @@ -66,7 +67,10 @@ class FeaturesExtractor: self, solver: "InternalSolver", sample: Sample, + lp_stats: LPSolveStats, ) -> None: + for (k, v) in lp_stats.__dict__.items(): + sample.put_scalar(k, v) variables = solver.get_variables(with_static=False, with_sa=self.with_sa) constraints = solver.get_constraints(with_static=False, with_sa=self.with_sa) sample.put_array("lp_var_basis_status", variables.basis_status) @@ -113,15 +117,21 @@ class FeaturesExtractor: ], ), ) - static_instance_features = sample.get_vector("static_instance_features") + + # Build lp_instance_features + static_instance_features = sample.get_array("static_instance_features") assert static_instance_features is not None - sample.put_vector( + assert lp_stats.lp_value is not None + assert lp_stats.lp_wallclock_time is not None + sample.put_array( "lp_instance_features", - static_instance_features - + [ - sample.get_scalar("lp_value"), - sample.get_scalar("lp_wallclock_time"), - ], + np.hstack( + [ + static_instance_features, + lp_stats.lp_value, + lp_stats.lp_wallclock_time, + ] + ), ) def extract_after_mip_features( @@ -241,30 +251,22 @@ class FeaturesExtractor: else: lazy.append(False) sample.put_vector_list("static_constr_features", user_features) - sample.put_array("static_constr_lazy", np.array(lazy, dtype=bool)) sample.put_array("static_constr_categories", np.array(categories, dtype="S")) + constr_lazy = np.array(lazy, dtype=bool) + sample.put_array("static_constr_lazy", constr_lazy) + sample.put_scalar("static_constr_lazy_count", int(constr_lazy.sum())) def _extract_user_features_instance( self, instance: "Instance", sample: Sample, ) -> None: - user_features = instance.get_instance_features() - if isinstance(user_features, np.ndarray): - user_features = user_features.tolist() - assert isinstance(user_features, list), ( - f"Instance features must be a list. " - f"Found {type(user_features).__name__} instead." - ) - for v in user_features: - assert isinstance(v, numbers.Real), ( - f"Instance features must be a list of numbers. " - f"Found {type(v).__name__} instead." - ) - constr_lazy = sample.get_array("static_constr_lazy") - assert constr_lazy is not None - sample.put_vector("static_instance_features", user_features) - sample.put_scalar("static_constr_lazy_count", int(sum(constr_lazy))) + features = cast(np.ndarray, instance.get_instance_features()) + if isinstance(features, list): + features = np.array(features, dtype=float) + assert isinstance(features, np.ndarray) + assert features.dtype.kind in ["f"], f"Unsupported dtype: {features.dtype}" + sample.put_array("static_instance_features", features) # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based # approximation of strong branching. INFORMS Journal on Computing, 29(1), 185-195. diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index 648020a..5e54bee 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -3,8 +3,8 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging -import traceback import time +import traceback from typing import Optional, List, Any, cast, Dict, Tuple from p_tqdm import p_map @@ -15,7 +15,6 @@ from miplearn.components.dynamic_user_cuts import UserCutsComponent from miplearn.components.objective import ObjectiveValueComponent from miplearn.components.primal import PrimalSolutionComponent from miplearn.features.extractor import FeaturesExtractor -from miplearn.features.sample import Sample, MemorySample from miplearn.instance.base import Instance from miplearn.instance.picklegz import PickleGzInstance from miplearn.solvers import _RedirectOutput @@ -208,9 +207,9 @@ class LearningSolver: # ------------------------------------------------------- logger.info("Extracting features (after-lp)...") initial_time = time.time() - for (k, v) in lp_stats.__dict__.items(): - sample.put_scalar(k, v) - self.extractor.extract_after_lp_features(self.internal_solver, sample) + self.extractor.extract_after_lp_features( + self.internal_solver, sample, lp_stats + ) logger.info( "Features (after-lp) extracted in %.2f seconds" % (time.time() - initial_time) diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 25bbf7d..6e60f75 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -85,8 +85,8 @@ def test_knapsack() -> None: # after-lp # ------------------------------------------------------- - solver.solve_lp() - extractor.extract_after_lp_features(solver, sample) + lp_stats = solver.solve_lp() + extractor.extract_after_lp_features(solver, sample, lp_stats) assert_equals( sample.get_array("lp_var_basis_status"), np.array(["U", "B", "U", "L", "U"], dtype="S"), @@ -204,12 +204,12 @@ if __name__ == "__main__": solver = GurobiSolver() instance = MpsInstance(sys.argv[1]) solver.set_instance(instance) - solver.solve_lp(tee=True) + lp_stats = solver.solve_lp(tee=True) extractor = FeaturesExtractor(with_lhs=False) sample = Hdf5Sample("tmp/prof.h5", mode="w") def run() -> None: extractor.extract_after_load_features(instance, solver, sample) - extractor.extract_after_lp_features(solver, sample) + extractor.extract_after_lp_features(solver, sample, lp_stats) cProfile.run("run()", filename="tmp/prof") From 56b39b6c9c2f00bde27a0e1103dcf698358da5fa Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Mon, 9 Aug 2021 14:02:14 -0500 Subject: [PATCH 53/67] Make get_instance_features return np.ndarray --- miplearn/features/extractor.py | 17 ++++++++++++----- miplearn/instance/base.py | 6 ++++-- miplearn/instance/file.py | 3 ++- miplearn/instance/picklegz.py | 3 ++- miplearn/problems/knapsack.py | 4 ++-- miplearn/solvers/pyomo/base.py | 12 +++++++----- 6 files changed, 29 insertions(+), 16 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 184694a..ead7f8f 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -261,11 +261,18 @@ class FeaturesExtractor: instance: "Instance", sample: Sample, ) -> None: - features = cast(np.ndarray, instance.get_instance_features()) - if isinstance(features, list): - features = np.array(features, dtype=float) - assert isinstance(features, np.ndarray) - assert features.dtype.kind in ["f"], f"Unsupported dtype: {features.dtype}" + features = instance.get_instance_features() + assert isinstance(features, np.ndarray), ( + f"Instance features must be a numpy array. " + f"Found {features.__class__} instead." + ) + assert len(features.shape) == 1, ( + f"Instance features must be a vector. " + f"Found array with shape {features.shape} instead." + ) + assert features.dtype.kind in [ + "f" + ], f"Instance features have unsupported dtype: {features.dtype}" sample.put_array("static_instance_features", features) # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based diff --git a/miplearn/instance/base.py b/miplearn/instance/base.py index 3f0e7b2..1c5ba8a 100644 --- a/miplearn/instance/base.py +++ b/miplearn/instance/base.py @@ -6,6 +6,8 @@ import logging from abc import ABC, abstractmethod from typing import Any, List, TYPE_CHECKING, Dict +import numpy as np + from miplearn.features.sample import Sample, MemorySample logger = logging.getLogger(__name__) @@ -37,7 +39,7 @@ class Instance(ABC): """ pass - def get_instance_features(self) -> List[float]: + def get_instance_features(self) -> np.ndarray: """ Returns a 1-dimensional array of (numerical) features describing the entire instance. @@ -59,7 +61,7 @@ class Instance(ABC): By default, returns [0.0]. """ - return [0.0] + return np.zeros(1) def get_variable_features(self) -> Dict[str, List[float]]: """ diff --git a/miplearn/instance/file.py b/miplearn/instance/file.py index 14f9fdf..daf1816 100644 --- a/miplearn/instance/file.py +++ b/miplearn/instance/file.py @@ -6,6 +6,7 @@ import os from typing import Any, Optional, List, Dict, TYPE_CHECKING import pickle +import numpy as np from overrides import overrides from miplearn.features.sample import Hdf5Sample, Sample @@ -30,7 +31,7 @@ class FileInstance(Instance): return self.instance.to_model() @overrides - def get_instance_features(self) -> List[float]: + def get_instance_features(self) -> np.ndarray: assert self.instance is not None return self.instance.get_instance_features() diff --git a/miplearn/instance/picklegz.py b/miplearn/instance/picklegz.py index 8472a9d..b7b6b40 100644 --- a/miplearn/instance/picklegz.py +++ b/miplearn/instance/picklegz.py @@ -8,6 +8,7 @@ import os import pickle from typing import Optional, Any, List, cast, IO, TYPE_CHECKING, Dict +import numpy as np from overrides import overrides from miplearn.features.sample import Sample @@ -42,7 +43,7 @@ class PickleGzInstance(Instance): return self.instance.to_model() @overrides - def get_instance_features(self) -> List[float]: + def get_instance_features(self) -> np.ndarray: assert self.instance is not None return self.instance.get_instance_features() diff --git a/miplearn/problems/knapsack.py b/miplearn/problems/knapsack.py index 83df03f..2a922de 100644 --- a/miplearn/problems/knapsack.py +++ b/miplearn/problems/knapsack.py @@ -94,8 +94,8 @@ class MultiKnapsackInstance(Instance): return model @overrides - def get_instance_features(self) -> List[float]: - return [float(np.mean(self.prices))] + list(self.capacities) + def get_instance_features(self) -> np.ndarray: + return np.array([float(np.mean(self.prices))] + list(self.capacities)) @overrides def get_variable_features(self) -> Dict[str, List[float]]: diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index ed0ba59..acd0e37 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -622,11 +622,13 @@ class PyomoTestInstanceKnapsack(Instance): return model @overrides - def get_instance_features(self) -> List[float]: - return [ - self.capacity, - np.average(self.weights), - ] + def get_instance_features(self) -> np.ndarray: + return np.array( + [ + self.capacity, + np.average(self.weights), + ] + ) @overrides def get_variable_features(self) -> Dict[str, List[float]]: From 895cb962b6fce6bd97e400b430d0be72d9f2d39e Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Mon, 9 Aug 2021 15:19:53 -0500 Subject: [PATCH 54/67] Make get_variable_{categories,features} return np.ndarray --- miplearn/components/component.py | 6 +- miplearn/components/primal.py | 18 ++-- miplearn/features/extractor.py | 154 +++++++++++++++---------------- miplearn/instance/base.py | 12 +-- miplearn/instance/file.py | 8 +- miplearn/instance/picklegz.py | 8 +- miplearn/problems/knapsack.py | 12 ++- miplearn/problems/stab.py | 16 ++-- miplearn/problems/tsp.py | 4 - miplearn/solvers/pyomo/base.py | 21 +++-- miplearn/types.py | 2 +- tests/components/test_primal.py | 57 +++++++----- tests/features/test_extractor.py | 4 +- 13 files changed, 166 insertions(+), 156 deletions(-) diff --git a/miplearn/components/component.py b/miplearn/components/component.py index 3013d4e..d47be48 100644 --- a/miplearn/components/component.py +++ b/miplearn/components/component.py @@ -9,7 +9,7 @@ from p_tqdm import p_umap from miplearn.features.sample import Sample from miplearn.instance.base import Instance -from miplearn.types import LearningSolveStats +from miplearn.types import LearningSolveStats, Category if TYPE_CHECKING: from miplearn.solvers.learning import LearningSolver @@ -101,8 +101,8 @@ class Component: def fit_xy( self, - x: Dict[str, np.ndarray], - y: Dict[str, np.ndarray], + x: Dict[Category, np.ndarray], + y: Dict[Category, np.ndarray], ) -> None: """ Given two dictionaries x and y, mapping the name of the category to matrices diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index 2e40623..769cbef 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -47,8 +47,8 @@ class PrimalSolutionComponent(Component): assert isinstance(threshold, Threshold) assert mode in ["exact", "heuristic"] self.mode = mode - self.classifiers: Dict[str, Classifier] = {} - self.thresholds: Dict[str, Threshold] = {} + self.classifiers: Dict[Category, Classifier] = {} + self.thresholds: Dict[Category, Threshold] = {} self.threshold_prototype = threshold self.classifier_prototype = classifier @@ -96,7 +96,7 @@ class PrimalSolutionComponent(Component): def sample_predict(self, sample: Sample) -> Solution: var_names = sample.get_array("static_var_names") - var_categories = sample.get_vector("static_var_categories") + var_categories = sample.get_array("static_var_categories") assert var_names is not None assert var_categories is not None @@ -120,7 +120,7 @@ class PrimalSolutionComponent(Component): # Convert y_pred into solution solution: Solution = {v: None for v in var_names} - category_offset: Dict[str, int] = {cat: 0 for cat in x.keys()} + category_offset: Dict[Category, int] = {cat: 0 for cat in x.keys()} for (i, var_name) in enumerate(var_names): category = var_categories[i] if category not in category_offset: @@ -146,7 +146,7 @@ class PrimalSolutionComponent(Component): mip_var_values = sample.get_array("mip_var_values") var_features = sample.get_vector_list("lp_var_features") var_names = sample.get_array("static_var_names") - var_categories = sample.get_vector("static_var_categories") + var_categories = sample.get_array("static_var_categories") if var_features is None: var_features = sample.get_vector_list("static_var_features") assert instance_features is not None @@ -157,7 +157,7 @@ class PrimalSolutionComponent(Component): for (i, var_name) in enumerate(var_names): # Initialize categories category = var_categories[i] - if category is None: + if len(category) == 0: continue if category not in x.keys(): x[category] = [] @@ -176,7 +176,7 @@ class PrimalSolutionComponent(Component): f"Variable {var_name} has non-binary value {opt_value} in the " "optimal solution. Predicting values of non-binary " "variables is not currently supported. Please set its " - "category to None." + "category to ''." ) y[category].append([opt_value < 0.5, opt_value >= 0.5]) return x, y @@ -230,8 +230,8 @@ class PrimalSolutionComponent(Component): @overrides def fit_xy( self, - x: Dict[str, np.ndarray], - y: Dict[str, np.ndarray], + x: Dict[Category, np.ndarray], + y: Dict[Category, np.ndarray], ) -> None: for category in x.keys(): clf = self.classifier_prototype.clone() diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index ead7f8f..c82b950 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -46,20 +46,25 @@ class FeaturesExtractor: vars_features_user, var_categories = self._extract_user_features_vars( instance, sample ) - sample.put_vector("static_var_categories", var_categories) + sample.put_array("static_var_categories", var_categories) self._extract_user_features_constrs(instance, sample) self._extract_user_features_instance(instance, sample) alw17 = self._extract_var_features_AlvLouWeh2017(sample) - sample.put_vector_list( + + # Build static_var_features + assert variables.lower_bounds is not None + assert variables.obj_coeffs is not None + assert variables.upper_bounds is not None + sample.put_array( "static_var_features", - self._combine( + np.hstack( [ - alw17, vars_features_user, - sample.get_array("static_var_lower_bounds"), - sample.get_array("static_var_obj_coeffs"), - sample.get_array("static_var_upper_bounds"), - ], + alw17, + variables.lower_bounds.reshape(-1, 1), + variables.obj_coeffs.reshape(-1, 1), + variables.upper_bounds.reshape(-1, 1), + ] ), ) @@ -88,23 +93,29 @@ class FeaturesExtractor: sample.put_array("lp_constr_sa_rhs_up", constraints.sa_rhs_up) sample.put_array("lp_constr_slacks", constraints.slacks) alw17 = self._extract_var_features_AlvLouWeh2017(sample) - sample.put_vector_list( - "lp_var_features", - self._combine( - [ - alw17, - sample.get_array("lp_var_reduced_costs"), - sample.get_array("lp_var_sa_lb_down"), - sample.get_array("lp_var_sa_lb_up"), - sample.get_array("lp_var_sa_obj_down"), - sample.get_array("lp_var_sa_obj_up"), - sample.get_array("lp_var_sa_ub_down"), - sample.get_array("lp_var_sa_ub_up"), - sample.get_array("lp_var_values"), - sample.get_vector_list("static_var_features"), - ], - ), - ) + + # Build lp_var_features + lp_var_features_list = [] + for f in [ + sample.get_array("static_var_features"), + alw17, + ]: + if f is not None: + lp_var_features_list.append(f) + for f in [ + variables.reduced_costs, + variables.sa_lb_down, + variables.sa_lb_up, + variables.sa_obj_down, + variables.sa_obj_up, + variables.sa_ub_down, + variables.sa_ub_up, + variables.values, + ]: + if f is not None: + lp_var_features_list.append(f.reshape(-1, 1)) + sample.put_array("lp_var_features", np.hstack(lp_var_features_list)) + sample.put_vector_list( "lp_constr_features", self._combine( @@ -148,60 +159,49 @@ class FeaturesExtractor: self, instance: "Instance", sample: Sample, - ) -> Tuple[List, List]: + ) -> Tuple[np.ndarray, np.ndarray]: # Query variable names var_names = sample.get_array("static_var_names") assert var_names is not None - # Query variable features and categories - var_features_dict = { - v.encode(): f for (v, f) in instance.get_variable_features().items() - } - var_categories_dict = { - v.encode(): f for (v, f) in instance.get_variable_categories().items() - } - - # Assert that variables in user-provided dicts actually exist - var_names_set = set(var_names) - for keys in [var_features_dict.keys(), var_categories_dict.keys()]: - for vn in cast(KeysView, keys): - assert ( - vn in var_names_set - ), f"Variable {vn!r} not found in the problem; {var_names_set}" - - # Assemble into compact lists - user_features: List[Optional[List[float]]] = [] - categories: List[Optional[str]] = [] - for (i, var_name) in enumerate(var_names): - if var_name not in var_categories_dict: - user_features.append(None) - categories.append(None) - continue - category: str = var_categories_dict[var_name] - assert isinstance(category, str), ( - f"Variable category must be a string. " - f"Found {type(category).__name__} instead for var={var_name}." - ) - categories.append(category) - user_features_i: Optional[List[float]] = None - if var_name in var_features_dict: - user_features_i = var_features_dict[var_name] - if isinstance(user_features_i, np.ndarray): - user_features_i = user_features_i.tolist() - assert isinstance(user_features_i, list), ( - f"Variable features must be a list. " - f"Found {type(user_features_i).__name__} instead for " - f"var={var_name}." - ) - for v in user_features_i: - assert isinstance(v, numbers.Real), ( - f"Variable features must be a list of numbers. " - f"Found {type(v).__name__} instead " - f"for var={var_name}." - ) - user_features_i = list(user_features_i) - user_features.append(user_features_i) - return user_features, categories + # Query variable features + var_features = instance.get_variable_features(var_names) + assert isinstance(var_features, np.ndarray), ( + f"Variable features must be a numpy array. " + f"Found {var_features.__class__} instead." + ) + assert len(var_features.shape) == 2, ( + f"Variable features must be 2-dimensional array. " + f"Found array with shape {var_features.shape} instead." + ) + assert var_features.shape[0] == len(var_names), ( + f"Variable features must have exactly {len(var_names)} rows. " + f"Found {var_features.shape[0]} rows instead." + ) + assert var_features.dtype.kind in ["f"], ( + f"Variable features must be floating point numbers. " + f"Found dtype: {var_features.dtype} instead." + ) + + # Query variable categories + var_categories = instance.get_variable_categories(var_names) + assert isinstance(var_categories, np.ndarray), ( + f"Variable categories must be a numpy array. " + f"Found {var_categories.__class__} instead." + ) + assert len(var_categories.shape) == 1, ( + f"Variable categories must be a vector. " + f"Found array with shape {var_categories.shape} instead." + ) + assert len(var_categories) == len(var_names), ( + f"Variable categories must have exactly {len(var_names)} elements. " + f"Found {var_features.shape[0]} elements instead." + ) + assert var_categories.dtype.kind == "S", ( + f"Variable categories must be a numpy array with dtype='S'. " + f"Found {var_categories.dtype} instead." + ) + return var_features, var_categories def _extract_user_features_constrs( self, @@ -277,7 +277,7 @@ class FeaturesExtractor: # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based # approximation of strong branching. INFORMS Journal on Computing, 29(1), 185-195. - def _extract_var_features_AlvLouWeh2017(self, sample: Sample) -> List: + def _extract_var_features_AlvLouWeh2017(self, sample: Sample) -> np.ndarray: obj_coeffs = sample.get_array("static_var_obj_coeffs") obj_sa_down = sample.get_array("lp_var_sa_obj_down") obj_sa_up = sample.get_array("lp_var_sa_obj_up") @@ -351,7 +351,7 @@ class FeaturesExtractor: f[i] = 0.0 features.append(f) - return features + return np.array(features, dtype=float) def _combine( self, diff --git a/miplearn/instance/base.py b/miplearn/instance/base.py index 1c5ba8a..09e0397 100644 --- a/miplearn/instance/base.py +++ b/miplearn/instance/base.py @@ -63,7 +63,7 @@ class Instance(ABC): """ return np.zeros(1) - def get_variable_features(self) -> Dict[str, List[float]]: + def get_variable_features(self, names: np.ndarray) -> np.ndarray: """ Returns dictionary mapping the name of each variable to a (1-dimensional) list of numerical features describing a particular decision variable. @@ -81,11 +81,11 @@ class Instance(ABC): If features are not provided for a given variable, MIPLearn will use a default set of features. - By default, returns {}. + By default, returns [[0.0], ..., [0.0]]. """ - return {} + return np.zeros((len(names), 1)) - def get_variable_categories(self) -> Dict[str, str]: + def get_variable_categories(self, names: np.ndarray) -> np.ndarray: """ Returns a dictionary mapping the name of each variable to its category. @@ -93,9 +93,9 @@ class Instance(ABC): internal ML model to predict the values of both variables. If a variable is not listed in the dictionary, ML models will ignore the variable. - By default, returns {}. + By default, returns `names`. """ - return {} + return names def get_constraint_features(self) -> Dict[str, List[float]]: return {} diff --git a/miplearn/instance/file.py b/miplearn/instance/file.py index daf1816..d7181f2 100644 --- a/miplearn/instance/file.py +++ b/miplearn/instance/file.py @@ -36,14 +36,14 @@ class FileInstance(Instance): return self.instance.get_instance_features() @overrides - def get_variable_features(self) -> Dict[str, List[float]]: + def get_variable_features(self, names: np.ndarray) -> np.ndarray: assert self.instance is not None - return self.instance.get_variable_features() + return self.instance.get_variable_features(names) @overrides - def get_variable_categories(self) -> Dict[str, str]: + def get_variable_categories(self, names: np.ndarray) -> np.ndarray: assert self.instance is not None - return self.instance.get_variable_categories() + return self.instance.get_variable_categories(names) @overrides def get_constraint_features(self) -> Dict[str, List[float]]: diff --git a/miplearn/instance/picklegz.py b/miplearn/instance/picklegz.py index b7b6b40..94b3968 100644 --- a/miplearn/instance/picklegz.py +++ b/miplearn/instance/picklegz.py @@ -48,14 +48,14 @@ class PickleGzInstance(Instance): return self.instance.get_instance_features() @overrides - def get_variable_features(self) -> Dict[str, List[float]]: + def get_variable_features(self, names: np.ndarray) -> np.ndarray: assert self.instance is not None - return self.instance.get_variable_features() + return self.instance.get_variable_features(names) @overrides - def get_variable_categories(self) -> Dict[str, str]: + def get_variable_categories(self, names: np.ndarray) -> np.ndarray: assert self.instance is not None - return self.instance.get_variable_categories() + return self.instance.get_variable_categories(names) @overrides def get_constraint_features(self) -> Dict[str, List[float]]: diff --git a/miplearn/problems/knapsack.py b/miplearn/problems/knapsack.py index 2a922de..1dd06ef 100644 --- a/miplearn/problems/knapsack.py +++ b/miplearn/problems/knapsack.py @@ -98,11 +98,13 @@ class MultiKnapsackInstance(Instance): return np.array([float(np.mean(self.prices))] + list(self.capacities)) @overrides - def get_variable_features(self) -> Dict[str, List[float]]: - return { - f"x[{i}]": [self.prices[i] + list(self.weights[:, i])] - for i in range(self.n) - } + def get_variable_features(self, names: np.ndarray) -> np.ndarray: + features = [] + for i in range(len(self.weights)): + f = [self.prices[i]] + f.extend(self.weights[:, i]) + features.append(f) + return np.array(features) # noinspection PyPep8Naming diff --git a/miplearn/problems/stab.py b/miplearn/problems/stab.py index db5bff8..a64fb3c 100644 --- a/miplearn/problems/stab.py +++ b/miplearn/problems/stab.py @@ -66,9 +66,11 @@ class MaxWeightStableSetInstance(Instance): return model @overrides - def get_variable_features(self) -> Dict[str, List[float]]: - features = {} - for v1 in self.nodes: + def get_variable_features(self, names: np.ndarray) -> np.ndarray: + features = [] + assert len(names) == len(self.nodes) + for i, v1 in enumerate(self.nodes): + assert names[i] == f"x[{v1}]".encode() neighbor_weights = [0.0] * 15 neighbor_degrees = [100.0] * 15 for v2 in self.graph.neighbors(v1): @@ -80,12 +82,12 @@ class MaxWeightStableSetInstance(Instance): f += neighbor_weights[:5] f += neighbor_degrees[:5] f += [self.graph.degree(v1)] - features[f"x[{v1}]"] = f - return features + features.append(f) + return np.array(features) @overrides - def get_variable_categories(self) -> Dict[str, str]: - return {f"x[{v}]": "default" for v in self.nodes} + def get_variable_categories(self, names: np.ndarray) -> np.ndarray: + return np.array(["default" for _ in names], dtype="S") class MaxWeightStableSetGenerator: diff --git a/miplearn/problems/tsp.py b/miplearn/problems/tsp.py index 66cae5d..bdb053b 100644 --- a/miplearn/problems/tsp.py +++ b/miplearn/problems/tsp.py @@ -80,10 +80,6 @@ class TravelingSalesmanInstance(Instance): ) return model - @overrides - def get_variable_categories(self) -> Dict[str, str]: - return {f"x[{e}]": f"x[{e}]" for e in self.edges} - @overrides def find_violated_lazy_constraints( self, diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index acd0e37..3e306a5 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -605,6 +605,7 @@ class PyomoTestInstanceKnapsack(Instance): self.weights = weights self.prices = prices self.capacity = capacity + self.n = len(weights) @overrides def to_model(self) -> pe.ConcreteModel: @@ -631,15 +632,17 @@ class PyomoTestInstanceKnapsack(Instance): ) @overrides - def get_variable_features(self) -> Dict[str, List[float]]: - return { - f"x[{i}]": [ - self.weights[i], - self.prices[i], + def get_variable_features(self, names: np.ndarray) -> np.ndarray: + return np.vstack( + [ + [[self.weights[i], self.prices[i]] for i in range(self.n)], + [0.0, 0.0], ] - for i in range(len(self.weights)) - } + ) @overrides - def get_variable_categories(self) -> Dict[str, str]: - return {f"x[{i}]": "default" for i in range(len(self.weights))} + def get_variable_categories(self, names: np.ndarray) -> np.ndarray: + return np.array( + ["default" if n.decode().startswith("x") else "" for n in names], + dtype="S", + ) diff --git a/miplearn/types.py b/miplearn/types.py index 5d94163..74a194e 100644 --- a/miplearn/types.py +++ b/miplearn/types.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: # noinspection PyUnresolvedReferences from miplearn.solvers.learning import InternalSolver -Category = str +Category = bytes IterationCallback = Callable[[], bool] LazyCallback = Callable[[Any, Any], None] SolverParams = Dict[str, Any] diff --git a/tests/components/test_primal.py b/tests/components/test_primal.py index 0f56d7b..a77cad0 100644 --- a/tests/components/test_primal.py +++ b/tests/components/test_primal.py @@ -23,21 +23,28 @@ def sample() -> Sample: sample = MemorySample( { "static_var_names": np.array(["x[0]", "x[1]", "x[2]", "x[3]"], dtype="S"), - "static_var_categories": ["default", None, "default", "default"], + "static_var_categories": np.array( + ["default", "", "default", "default"], + dtype="S", + ), "mip_var_values": np.array([0.0, 1.0, 1.0, 0.0]), - "static_instance_features": [5.0], - "static_var_features": [ - [0.0, 0.0], - None, - [1.0, 0.0], - [1.0, 1.0], - ], - "lp_var_features": [ - [0.0, 0.0, 2.0, 2.0], - None, - [1.0, 0.0, 3.0, 2.0], - [1.0, 1.0, 3.0, 3.0], - ], + "static_instance_features": np.array([5.0]), + "static_var_features": np.array( + [ + [0.0, 0.0], + [0.0, 0.0], + [1.0, 0.0], + [1.0, 1.0], + ] + ), + "lp_var_features": np.array( + [ + [0.0, 0.0, 2.0, 2.0], + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 3.0, 2.0], + [1.0, 1.0, 3.0, 3.0], + ] + ), }, ) return sample @@ -45,14 +52,14 @@ def sample() -> Sample: def test_xy(sample: Sample) -> None: x_expected = { - "default": [ + b"default": [ [5.0, 0.0, 0.0, 2.0, 2.0], [5.0, 1.0, 0.0, 3.0, 2.0], [5.0, 1.0, 1.0, 3.0, 3.0], ] } y_expected = { - "default": [ + b"default": [ [True, False], [False, True], [True, False], @@ -72,15 +79,15 @@ def test_fit_xy() -> None: thr.clone = lambda: Mock(spec=Threshold) comp = PrimalSolutionComponent(classifier=clf, threshold=thr) x = { - "type-a": np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), - "type-b": np.array([[7.0, 8.0, 9.0]]), + b"type-a": np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + b"type-b": np.array([[7.0, 8.0, 9.0]]), } y = { - "type-a": np.array([[True, False], [False, True]]), - "type-b": np.array([[True, False]]), + b"type-a": np.array([[True, False], [False, True]]), + b"type-b": np.array([[True, False]]), } comp.fit_xy(x, y) - for category in ["type-a", "type-b"]: + for category in [b"type-a", b"type-b"]: assert category in comp.classifiers assert category in comp.thresholds clf = comp.classifiers[category] # type: ignore @@ -142,13 +149,13 @@ def test_predict(sample: Sample) -> None: thr.predict = Mock(return_value=[0.75, 0.75]) comp = PrimalSolutionComponent() x, _ = comp.sample_xy(None, sample) - comp.classifiers = {"default": clf} - comp.thresholds = {"default": thr} + comp.classifiers = {b"default": clf} + comp.thresholds = {b"default": thr} pred = comp.sample_predict(sample) clf.predict_proba.assert_called_once() thr.predict.assert_called_once() - assert_array_equal(x["default"], clf.predict_proba.call_args[0][0]) - assert_array_equal(x["default"], thr.predict.call_args[0][0]) + assert_array_equal(x[b"default"], clf.predict_proba.call_args[0][0]) + assert_array_equal(x[b"default"], thr.predict.call_args[0][0]) assert pred == { b"x[0]": 0.0, b"x[1]": None, diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 6e60f75..9d0da22 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -49,8 +49,8 @@ def test_knapsack() -> None: sample.get_vector("static_var_upper_bounds"), [1.0, 1.0, 1.0, 1.0, 67.0] ) assert_equals( - sample.get_vector("static_var_categories"), - ["default", "default", "default", "default", None], + sample.get_array("static_var_categories"), + np.array(["default", "default", "default", "default", ""], dtype="S"), ) assert sample.get_vector_list("static_var_features") is not None assert_equals( From e852d5cdcaf81d39a5ca641169e2b847e1a82e35 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Mon, 9 Aug 2021 20:11:37 -0500 Subject: [PATCH 55/67] Use np.ndarray for constraint methods in Instance --- miplearn/components/dynamic_common.py | 124 +++++------ miplearn/components/dynamic_lazy.py | 14 +- miplearn/components/dynamic_user_cuts.py | 14 +- miplearn/components/static_lazy.py | 35 +-- miplearn/features/extractor.py | 213 ++++++++++--------- miplearn/features/sample.py | 2 +- miplearn/instance/base.py | 24 +-- miplearn/instance/file.py | 26 +-- miplearn/instance/picklegz.py | 26 +-- miplearn/problems/tsp.py | 9 +- miplearn/solvers/tests/__init__.py | 2 +- miplearn/types.py | 2 + tests/components/test_dynamic_lazy.py | 157 ++++---------- tests/components/test_dynamic_user_cuts.py | 9 +- tests/components/test_static_lazy.py | 71 ++++--- tests/features/test_extractor.py | 235 +++++++++++++++++++-- 16 files changed, 533 insertions(+), 430 deletions(-) diff --git a/miplearn/components/dynamic_common.py b/miplearn/components/dynamic_common.py index bc28438..0b48f6f 100644 --- a/miplearn/components/dynamic_common.py +++ b/miplearn/components/dynamic_common.py @@ -8,12 +8,14 @@ from typing import Dict, List, Tuple, Optional, Any, Set import numpy as np from overrides import overrides +from miplearn.features.extractor import FeaturesExtractor from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import Threshold from miplearn.components import classifier_evaluation_dict from miplearn.components.component import Component from miplearn.features.sample import Sample from miplearn.instance.base import Instance +from miplearn.types import ConstraintCategory, ConstraintName logger = logging.getLogger(__name__) @@ -32,9 +34,9 @@ class DynamicConstraintsComponent(Component): assert isinstance(classifier, Classifier) self.threshold_prototype: Threshold = threshold self.classifier_prototype: Classifier = classifier - self.classifiers: Dict[str, Classifier] = {} - self.thresholds: Dict[str, Threshold] = {} - self.known_cids: List[str] = [] + self.classifiers: Dict[ConstraintCategory, Classifier] = {} + self.thresholds: Dict[ConstraintCategory, Threshold] = {} + self.known_cids: List[ConstraintName] = [] self.attr = attr def sample_xy_with_cids( @@ -42,51 +44,45 @@ class DynamicConstraintsComponent(Component): instance: Optional[Instance], sample: Sample, ) -> Tuple[ - Dict[str, List[List[float]]], - Dict[str, List[List[bool]]], - Dict[str, List[str]], + Dict[ConstraintCategory, List[List[float]]], + Dict[ConstraintCategory, List[List[bool]]], + Dict[ConstraintCategory, List[ConstraintName]], ]: + if len(self.known_cids) == 0: + return {}, {}, {} assert instance is not None - x: Dict[str, List[List[float]]] = {} - y: Dict[str, List[List[bool]]] = {} - cids: Dict[str, List[str]] = {} - constr_categories_dict = instance.get_constraint_categories() - constr_features_dict = instance.get_constraint_features() + x: Dict[ConstraintCategory, List[List[float]]] = {} + y: Dict[ConstraintCategory, List[List[bool]]] = {} + cids: Dict[ConstraintCategory, List[ConstraintName]] = {} + known_cids = np.array(self.known_cids, dtype="S") + + # Get user-provided constraint features + ( + constr_features, + constr_categories, + constr_lazy, + ) = FeaturesExtractor._extract_user_features_constrs(instance, known_cids) + + # Augment with instance features instance_features = sample.get_array("static_instance_features") assert instance_features is not None - for cid in self.known_cids: - # Initialize categories - if cid in constr_categories_dict: - category = constr_categories_dict[cid] - else: - category = cid - if category is None: - continue - if category not in x: - x[category] = [] - y[category] = [] - cids[category] = [] - - # Features - features: List[float] = [] - features.extend(instance_features) - if cid in constr_features_dict: - features.extend(constr_features_dict[cid]) - for ci in features: - assert isinstance(ci, float), ( - f"Constraint features must be a list of floats. " - f"Found {ci.__class__.__name__} instead." - ) - x[category].append(features) - cids[category].append(cid) - - # Labels - enforced_cids = sample.get_set(self.attr) + constr_features = np.hstack( + [ + instance_features.reshape(1, -1).repeat(len(known_cids), axis=0), + constr_features, + ] + ) + assert len(known_cids) == constr_features.shape[0] + + categories = np.unique(constr_categories) + for c in categories: + x[c] = constr_features[constr_categories == c].tolist() + cids[c] = known_cids[constr_categories == c].tolist() + enforced_cids = np.array(list(sample.get_set(self.attr)), dtype="S") if enforced_cids is not None: - if cid in enforced_cids: - y[category] += [[False, True]] - else: - y[category] += [[True, False]] + tmp = np.isin(cids[c], enforced_cids).reshape(-1, 1) + y[c] = np.hstack([~tmp, tmp]).tolist() # type: ignore + return x, y, cids @overrides @@ -111,8 +107,8 @@ class DynamicConstraintsComponent(Component): self, instance: Instance, sample: Sample, - ) -> List[str]: - pred: List[str] = [] + ) -> List[ConstraintName]: + pred: List[ConstraintName] = [] if len(self.known_cids) == 0: logger.info("Classifiers not fitted. Skipping.") return pred @@ -137,8 +133,8 @@ class DynamicConstraintsComponent(Component): @overrides def fit_xy( self, - x: Dict[str, np.ndarray], - y: Dict[str, np.ndarray], + x: Dict[ConstraintCategory, np.ndarray], + y: Dict[ConstraintCategory, np.ndarray], ) -> None: for category in x.keys(): self.classifiers[category] = self.classifier_prototype.clone() @@ -153,40 +149,20 @@ class DynamicConstraintsComponent(Component): self, instance: Instance, sample: Sample, - ) -> Dict[str, Dict[str, float]]: + ) -> Dict[str, float]: actual = sample.get_set(self.attr) assert actual is not None pred = set(self.sample_predict(instance, sample)) - tp: Dict[str, int] = {} - tn: Dict[str, int] = {} - fp: Dict[str, int] = {} - fn: Dict[str, int] = {} - constr_categories_dict = instance.get_constraint_categories() + tp, tn, fp, fn = 0, 0, 0, 0 for cid in self.known_cids: - if cid not in constr_categories_dict: - continue - category = constr_categories_dict[cid] - if category not in tp.keys(): - tp[category] = 0 - tn[category] = 0 - fp[category] = 0 - fn[category] = 0 if cid in pred: if cid in actual: - tp[category] += 1 + tp += 1 else: - fp[category] += 1 + fp += 1 else: if cid in actual: - fn[category] += 1 + fn += 1 else: - tn[category] += 1 - return { - category: classifier_evaluation_dict( - tp=tp[category], - tn=tn[category], - fp=fp[category], - fn=fn[category], - ) - for category in tp.keys() - } + tn += 1 + return classifier_evaluation_dict(tp=tp, tn=tn, fp=fp, fn=fn) diff --git a/miplearn/components/dynamic_lazy.py b/miplearn/components/dynamic_lazy.py index 9110524..9e82556 100644 --- a/miplearn/components/dynamic_lazy.py +++ b/miplearn/components/dynamic_lazy.py @@ -15,7 +15,7 @@ from miplearn.components.component import Component from miplearn.components.dynamic_common import DynamicConstraintsComponent from miplearn.features.sample import Sample from miplearn.instance.base import Instance -from miplearn.types import LearningSolveStats +from miplearn.types import LearningSolveStats, ConstraintName, ConstraintCategory logger = logging.getLogger(__name__) @@ -41,11 +41,11 @@ class DynamicLazyConstraintsComponent(Component): self.classifiers = self.dynamic.classifiers self.thresholds = self.dynamic.thresholds self.known_cids = self.dynamic.known_cids - self.lazy_enforced: Set[str] = set() + self.lazy_enforced: Set[ConstraintName] = set() @staticmethod def enforce( - cids: List[str], + cids: List[ConstraintName], instance: Instance, model: Any, solver: "LearningSolver", @@ -117,7 +117,7 @@ class DynamicLazyConstraintsComponent(Component): self, instance: Instance, sample: Sample, - ) -> List[str]: + ) -> List[ConstraintName]: return self.dynamic.sample_predict(instance, sample) @overrides @@ -127,8 +127,8 @@ class DynamicLazyConstraintsComponent(Component): @overrides def fit_xy( self, - x: Dict[str, np.ndarray], - y: Dict[str, np.ndarray], + x: Dict[ConstraintCategory, np.ndarray], + y: Dict[ConstraintCategory, np.ndarray], ) -> None: self.dynamic.fit_xy(x, y) @@ -137,5 +137,5 @@ class DynamicLazyConstraintsComponent(Component): self, instance: Instance, sample: Sample, - ) -> Dict[str, Dict[str, float]]: + ) -> Dict[ConstraintCategory, Dict[str, float]]: return self.dynamic.sample_evaluate(instance, sample) diff --git a/miplearn/components/dynamic_user_cuts.py b/miplearn/components/dynamic_user_cuts.py index 5f69b04..3fe3298 100644 --- a/miplearn/components/dynamic_user_cuts.py +++ b/miplearn/components/dynamic_user_cuts.py @@ -15,7 +15,7 @@ from miplearn.components.component import Component from miplearn.components.dynamic_common import DynamicConstraintsComponent from miplearn.features.sample import Sample from miplearn.instance.base import Instance -from miplearn.types import LearningSolveStats +from miplearn.types import LearningSolveStats, ConstraintName, ConstraintCategory logger = logging.getLogger(__name__) @@ -34,7 +34,7 @@ class UserCutsComponent(Component): threshold=threshold, attr="mip_user_cuts_enforced", ) - self.enforced: Set[str] = set() + self.enforced: Set[ConstraintName] = set() self.n_added_in_callback = 0 @overrides @@ -71,7 +71,7 @@ class UserCutsComponent(Component): for cid in cids: if cid in self.enforced: continue - assert isinstance(cid, str) + assert isinstance(cid, ConstraintName) instance.enforce_user_cut(solver.internal_solver, model, cid) self.enforced.add(cid) self.n_added_in_callback += 1 @@ -110,7 +110,7 @@ class UserCutsComponent(Component): self, instance: "Instance", sample: Sample, - ) -> List[str]: + ) -> List[ConstraintName]: return self.dynamic.sample_predict(instance, sample) @overrides @@ -120,8 +120,8 @@ class UserCutsComponent(Component): @overrides def fit_xy( self, - x: Dict[str, np.ndarray], - y: Dict[str, np.ndarray], + x: Dict[ConstraintCategory, np.ndarray], + y: Dict[ConstraintCategory, np.ndarray], ) -> None: self.dynamic.fit_xy(x, y) @@ -130,5 +130,5 @@ class UserCutsComponent(Component): self, instance: "Instance", sample: Sample, - ) -> Dict[str, Dict[str, float]]: + ) -> Dict[ConstraintCategory, Dict[str, float]]: return self.dynamic.sample_evaluate(instance, sample) diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 8a85027..efc11d8 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -15,7 +15,7 @@ from miplearn.components.component import Component from miplearn.features.sample import Sample from miplearn.solvers.internal import Constraints from miplearn.instance.base import Instance -from miplearn.types import LearningSolveStats +from miplearn.types import LearningSolveStats, ConstraintName, ConstraintCategory logger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ if TYPE_CHECKING: class LazyConstraint: - def __init__(self, cid: str, obj: Any) -> None: + def __init__(self, cid: ConstraintName, obj: Any) -> None: self.cid = cid self.obj = obj @@ -44,11 +44,11 @@ class StaticLazyConstraintsComponent(Component): assert isinstance(classifier, Classifier) self.classifier_prototype: Classifier = classifier self.threshold_prototype: Threshold = threshold - self.classifiers: Dict[str, Classifier] = {} - self.thresholds: Dict[str, Threshold] = {} + self.classifiers: Dict[ConstraintCategory, Classifier] = {} + self.thresholds: Dict[ConstraintCategory, Threshold] = {} self.pool: Constraints = Constraints() self.violation_tolerance: float = violation_tolerance - self.enforced_cids: Set[str] = set() + self.enforced_cids: Set[ConstraintName] = set() self.n_restored: int = 0 self.n_iterations: int = 0 @@ -105,8 +105,8 @@ class StaticLazyConstraintsComponent(Component): @overrides def fit_xy( self, - x: Dict[str, np.ndarray], - y: Dict[str, np.ndarray], + x: Dict[ConstraintCategory, np.ndarray], + y: Dict[ConstraintCategory, np.ndarray], ) -> None: for c in y.keys(): assert c in x @@ -136,9 +136,9 @@ class StaticLazyConstraintsComponent(Component): ) -> None: self._check_and_add(solver) - def sample_predict(self, sample: Sample) -> List[str]: + def sample_predict(self, sample: Sample) -> List[ConstraintName]: x, y, cids = self._sample_xy_with_cids(sample) - enforced_cids: List[str] = [] + enforced_cids: List[ConstraintName] = [] for category in x.keys(): if category not in self.classifiers: continue @@ -156,7 +156,10 @@ class StaticLazyConstraintsComponent(Component): self, _: Optional[Instance], sample: Sample, - ) -> Tuple[Dict[str, List[List[float]]], Dict[str, List[List[float]]]]: + ) -> Tuple[ + Dict[ConstraintCategory, List[List[float]]], + Dict[ConstraintCategory, List[List[float]]], + ]: x, y, __ = self._sample_xy_with_cids(sample) return x, y @@ -197,13 +200,13 @@ class StaticLazyConstraintsComponent(Component): def _sample_xy_with_cids( self, sample: Sample ) -> Tuple[ - Dict[str, List[List[float]]], - Dict[str, List[List[float]]], - Dict[str, List[str]], + Dict[ConstraintCategory, List[List[float]]], + Dict[ConstraintCategory, List[List[float]]], + Dict[ConstraintCategory, List[ConstraintName]], ]: - x: Dict[str, List[List[float]]] = {} - y: Dict[str, List[List[float]]] = {} - cids: Dict[str, List[str]] = {} + x: Dict[ConstraintCategory, List[List[float]]] = {} + y: Dict[ConstraintCategory, List[List[float]]] = {} + cids: Dict[ConstraintCategory, List[ConstraintName]] = {} instance_features = sample.get_vector("static_instance_features") constr_features = sample.get_vector_list("lp_constr_features") constr_names = sample.get_array("static_constr_names") diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index c82b950..14fa673 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -2,10 +2,8 @@ # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -import collections -import numbers from math import log, isfinite -from typing import TYPE_CHECKING, Dict, Optional, List, Any, Tuple, KeysView, cast +from typing import TYPE_CHECKING, List, Tuple import numpy as np @@ -34,6 +32,7 @@ class FeaturesExtractor: ) -> None: variables = solver.get_variables(with_static=True) constraints = solver.get_constraints(with_static=True, with_lhs=self.with_lhs) + assert constraints.names is not None sample.put_array("static_var_lower_bounds", variables.lower_bounds) sample.put_array("static_var_names", variables.names) sample.put_array("static_var_obj_coeffs", variables.obj_coeffs) @@ -43,15 +42,30 @@ class FeaturesExtractor: # sample.put("static_constr_lhs", constraints.lhs) sample.put_array("static_constr_rhs", constraints.rhs) sample.put_array("static_constr_senses", constraints.senses) - vars_features_user, var_categories = self._extract_user_features_vars( - instance, sample - ) - sample.put_array("static_var_categories", var_categories) - self._extract_user_features_constrs(instance, sample) + + # Instance features self._extract_user_features_instance(instance, sample) - alw17 = self._extract_var_features_AlvLouWeh2017(sample) - # Build static_var_features + # Constraint features + ( + constr_features, + constr_categories, + constr_lazy, + ) = FeaturesExtractor._extract_user_features_constrs( + instance, + constraints.names, + ) + sample.put_array("static_constr_features", constr_features) + sample.put_array("static_constr_categories", constr_categories) + sample.put_array("static_constr_lazy", constr_lazy) + sample.put_scalar("static_constr_lazy_count", int(constr_lazy.sum())) + + # Variable features + ( + vars_features_user, + var_categories, + ) = self._extract_user_features_vars(instance, sample) + sample.put_array("static_var_categories", var_categories) assert variables.lower_bounds is not None assert variables.obj_coeffs is not None assert variables.upper_bounds is not None @@ -60,7 +74,7 @@ class FeaturesExtractor: np.hstack( [ vars_features_user, - alw17, + self._extract_var_features_AlvLouWeh2017(sample), variables.lower_bounds.reshape(-1, 1), variables.obj_coeffs.reshape(-1, 1), variables.upper_bounds.reshape(-1, 1), @@ -92,13 +106,12 @@ class FeaturesExtractor: sample.put_array("lp_constr_sa_rhs_down", constraints.sa_rhs_down) sample.put_array("lp_constr_sa_rhs_up", constraints.sa_rhs_up) sample.put_array("lp_constr_slacks", constraints.slacks) - alw17 = self._extract_var_features_AlvLouWeh2017(sample) - # Build lp_var_features + # Variable features lp_var_features_list = [] for f in [ sample.get_array("static_var_features"), - alw17, + self._extract_var_features_AlvLouWeh2017(sample), ]: if f is not None: lp_var_features_list.append(f) @@ -116,18 +129,20 @@ class FeaturesExtractor: lp_var_features_list.append(f.reshape(-1, 1)) sample.put_array("lp_var_features", np.hstack(lp_var_features_list)) - sample.put_vector_list( - "lp_constr_features", - self._combine( - [ - sample.get_vector_list("static_constr_features"), - sample.get_array("lp_constr_dual_values"), - sample.get_array("lp_constr_sa_rhs_down"), - sample.get_array("lp_constr_sa_rhs_up"), - sample.get_array("lp_constr_slacks"), - ], - ), - ) + # Constraint features + lp_constr_features_list = [] + for f in [sample.get_array("static_constr_features")]: + if f is not None: + lp_constr_features_list.append(f) + for f in [ + sample.get_array("lp_constr_dual_values"), + sample.get_array("lp_constr_sa_rhs_down"), + sample.get_array("lp_constr_sa_rhs_up"), + sample.get_array("lp_constr_slacks"), + ]: + if f is not None: + lp_constr_features_list.append(f.reshape(-1, 1)) + sample.put_array("lp_constr_features", np.hstack(lp_constr_features_list)) # Build lp_instance_features static_instance_features = sample.get_array("static_instance_features") @@ -155,6 +170,7 @@ class FeaturesExtractor: sample.put_array("mip_var_values", variables.values) sample.put_array("mip_constr_slacks", constraints.slacks) + # noinspection DuplicatedCode def _extract_user_features_vars( self, instance: "Instance", @@ -180,7 +196,7 @@ class FeaturesExtractor: ) assert var_features.dtype.kind in ["f"], ( f"Variable features must be floating point numbers. " - f"Found dtype: {var_features.dtype} instead." + f"Found {var_features.dtype} instead." ) # Query variable categories @@ -195,7 +211,7 @@ class FeaturesExtractor: ) assert len(var_categories) == len(var_names), ( f"Variable categories must have exactly {len(var_names)} elements. " - f"Found {var_features.shape[0]} elements instead." + f"Found {var_categories.shape[0]} elements instead." ) assert var_categories.dtype.kind == "S", ( f"Variable categories must be a numpy array with dtype='S'. " @@ -203,58 +219,71 @@ class FeaturesExtractor: ) return var_features, var_categories + # noinspection DuplicatedCode + @classmethod def _extract_user_features_constrs( - self, + cls, instance: "Instance", - sample: Sample, - ) -> None: - has_static_lazy = instance.has_static_lazy_constraints() - user_features: List[Optional[List[float]]] = [] - categories: List[Optional[bytes]] = [] - lazy: List[bool] = [] - constr_categories_dict = instance.get_constraint_categories() - constr_features_dict = instance.get_constraint_features() - constr_names = sample.get_array("static_constr_names") - assert constr_names is not None - - for (cidx, cname) in enumerate(constr_names): - category: Optional[str] = cname - if cname in constr_categories_dict: - category = constr_categories_dict[cname] - if category is None: - user_features.append(None) - categories.append(None) - continue - assert isinstance(category, bytes), ( - f"Constraint category must be bytes. " - f"Found {type(category).__name__} instead for cname={cname}.", - ) - categories.append(category) - cf: Optional[List[float]] = None - if cname in constr_features_dict: - cf = constr_features_dict[cname] - if isinstance(cf, np.ndarray): - cf = cf.tolist() - assert isinstance(cf, list), ( - f"Constraint features must be a list. " - f"Found {type(cf).__name__} instead for cname={cname}." - ) - 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}." - ) - cf = list(cf) - user_features.append(cf) - if has_static_lazy: - lazy.append(instance.is_constraint_lazy(cname)) - else: - lazy.append(False) - sample.put_vector_list("static_constr_features", user_features) - sample.put_array("static_constr_categories", np.array(categories, dtype="S")) - constr_lazy = np.array(lazy, dtype=bool) - sample.put_array("static_constr_lazy", constr_lazy) - sample.put_scalar("static_constr_lazy_count", int(constr_lazy.sum())) + constr_names: np.ndarray, + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + # Query constraint features + constr_features = instance.get_constraint_features(constr_names) + assert isinstance(constr_features, np.ndarray), ( + f"get_constraint_features must return a numpy array. " + f"Found {constr_features.__class__} instead." + ) + assert len(constr_features.shape) == 2, ( + f"get_constraint_features must return a 2-dimensional array. " + f"Found array with shape {constr_features.shape} instead." + ) + assert constr_features.shape[0] == len(constr_names), ( + f"get_constraint_features must return an array with {len(constr_names)} " + f"rows. Found {constr_features.shape[0]} rows instead." + ) + assert constr_features.dtype.kind in ["f"], ( + f"get_constraint_features must return floating point numbers. " + f"Found {constr_features.dtype} instead." + ) + + # Query constraint categories + constr_categories = instance.get_constraint_categories(constr_names) + assert isinstance(constr_categories, np.ndarray), ( + f"get_constraint_categories must return a numpy array. " + f"Found {constr_categories.__class__} instead." + ) + assert len(constr_categories.shape) == 1, ( + f"get_constraint_categories must return a vector. " + f"Found array with shape {constr_categories.shape} instead." + ) + assert len(constr_categories) == len(constr_names), ( + f"get_constraint_categories must return a vector with {len(constr_names)} " + f"elements. Found {constr_categories.shape[0]} elements instead." + ) + assert constr_categories.dtype.kind == "S", ( + f"get_constraint_categories must return a numpy array with dtype='S'. " + f"Found {constr_categories.dtype} instead." + ) + + # Query constraint lazy attribute + constr_lazy = instance.are_constraints_lazy(constr_names) + assert isinstance(constr_lazy, np.ndarray), ( + f"are_constraints_lazy must return a numpy array. " + f"Found {constr_lazy.__class__} instead." + ) + assert len(constr_lazy.shape) == 1, ( + f"are_constraints_lazy must return a vector. " + f"Found array with shape {constr_lazy.shape} instead." + ) + assert constr_lazy.shape[0] == len(constr_names), ( + f"are_constraints_lazy must return a vector with {len(constr_names)} " + f"elements. Found {constr_lazy.shape[0]} elements instead." + ) + assert constr_lazy.dtype.kind == "b", ( + f"are_constraints_lazy must return a boolean array. " + f"Found {constr_lazy.dtype} instead." + ) + + return constr_features, constr_categories, constr_lazy def _extract_user_features_instance( self, @@ -272,7 +301,7 @@ class FeaturesExtractor: ) assert features.dtype.kind in [ "f" - ], f"Instance features have unsupported dtype: {features.dtype}" + ], f"Instance features have unsupported {features.dtype}" sample.put_array("static_instance_features", features) # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based @@ -352,29 +381,3 @@ class FeaturesExtractor: features.append(f) return np.array(features, dtype=float) - - def _combine( - self, - items: List, - ) -> List[List[float]]: - combined: List[List[float]] = [] - for series in items: - if series is None: - continue - if len(combined) == 0: - for i in range(len(series)): - combined.append([]) - for (i, s) in enumerate(series): - if s is None: - continue - elif isinstance(s, list): - combined[i].extend([_clip(sj) for sj in s]) - else: - combined[i].append(_clip(s)) - return combined - - -def _clip(vi: float) -> float: - if not isfinite(vi): - return max(min(vi, 1e20), -1e20) - return vi diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 4842bc5..d3b8d65 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -103,7 +103,7 @@ class Sample(ABC): def _assert_is_scalar(self, value: Any) -> None: if value is None: return - if isinstance(value, (str, bool, int, float, np.bytes_)): + if isinstance(value, (str, bool, int, float, bytes, np.bytes_)): return assert False, f"scalar expected; found instead: {value} ({value.__class__})" diff --git a/miplearn/instance/base.py b/miplearn/instance/base.py index 09e0397..01f75e4 100644 --- a/miplearn/instance/base.py +++ b/miplearn/instance/base.py @@ -9,6 +9,7 @@ from typing import Any, List, TYPE_CHECKING, Dict import numpy as np from miplearn.features.sample import Sample, MemorySample +from miplearn.types import ConstraintName, ConstraintCategory logger = logging.getLogger(__name__) @@ -97,26 +98,23 @@ class Instance(ABC): """ return names - def get_constraint_features(self) -> Dict[str, List[float]]: - return {} - - def get_constraint_categories(self) -> Dict[str, str]: - return {} + def get_constraint_features(self, names: np.ndarray) -> np.ndarray: + return np.zeros((len(names), 1)) - def has_static_lazy_constraints(self) -> bool: - return False + def get_constraint_categories(self, names: np.ndarray) -> np.ndarray: + return names def has_dynamic_lazy_constraints(self) -> bool: return False - def is_constraint_lazy(self, cid: str) -> bool: - return False + def are_constraints_lazy(self, names: np.ndarray) -> np.ndarray: + return np.zeros(len(names), dtype=bool) def find_violated_lazy_constraints( self, solver: "InternalSolver", model: Any, - ) -> List[str]: + ) -> List[ConstraintName]: """ Returns lazy constraint violations found for the current solution. @@ -142,7 +140,7 @@ class Instance(ABC): self, solver: "InternalSolver", model: Any, - violation: str, + violation: ConstraintName, ) -> None: """ Adds constraints to the model to ensure that the given violation is fixed. @@ -168,14 +166,14 @@ class Instance(ABC): def has_user_cuts(self) -> bool: return False - def find_violated_user_cuts(self, model: Any) -> List[str]: + def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]: return [] def enforce_user_cut( self, solver: "InternalSolver", model: Any, - violation: str, + violation: ConstraintName, ) -> Any: return None diff --git a/miplearn/instance/file.py b/miplearn/instance/file.py index d7181f2..5c2615f 100644 --- a/miplearn/instance/file.py +++ b/miplearn/instance/file.py @@ -11,6 +11,7 @@ from overrides import overrides from miplearn.features.sample import Hdf5Sample, Sample from miplearn.instance.base import Instance +from miplearn.types import ConstraintName, ConstraintCategory if TYPE_CHECKING: from miplearn.solvers.learning import InternalSolver @@ -46,19 +47,14 @@ class FileInstance(Instance): return self.instance.get_variable_categories(names) @overrides - def get_constraint_features(self) -> Dict[str, List[float]]: + def get_constraint_features(self, names: np.ndarray) -> np.ndarray: assert self.instance is not None - return self.instance.get_constraint_features() + return self.instance.get_constraint_features(names) @overrides - def get_constraint_categories(self) -> Dict[str, str]: + def get_constraint_categories(self, names: np.ndarray) -> np.ndarray: assert self.instance is not None - return self.instance.get_constraint_categories() - - @overrides - def has_static_lazy_constraints(self) -> bool: - assert self.instance is not None - return self.instance.has_static_lazy_constraints() + return self.instance.get_constraint_categories(names) @overrides def has_dynamic_lazy_constraints(self) -> bool: @@ -66,16 +62,16 @@ class FileInstance(Instance): return self.instance.has_dynamic_lazy_constraints() @overrides - def is_constraint_lazy(self, cid: str) -> bool: + def are_constraints_lazy(self, names: np.ndarray) -> np.ndarray: assert self.instance is not None - return self.instance.is_constraint_lazy(cid) + return self.instance.are_constraints_lazy(names) @overrides def find_violated_lazy_constraints( self, solver: "InternalSolver", model: Any, - ) -> List[str]: + ) -> List[ConstraintName]: assert self.instance is not None return self.instance.find_violated_lazy_constraints(solver, model) @@ -84,13 +80,13 @@ class FileInstance(Instance): self, solver: "InternalSolver", model: Any, - violation: str, + violation: ConstraintName, ) -> None: assert self.instance is not None self.instance.enforce_lazy_constraint(solver, model, violation) @overrides - def find_violated_user_cuts(self, model: Any) -> List[str]: + def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]: assert self.instance is not None return self.instance.find_violated_user_cuts(model) @@ -99,7 +95,7 @@ class FileInstance(Instance): self, solver: "InternalSolver", model: Any, - violation: str, + violation: ConstraintName, ) -> None: assert self.instance is not None self.instance.enforce_user_cut(solver, model, violation) diff --git a/miplearn/instance/picklegz.py b/miplearn/instance/picklegz.py index 94b3968..41cf9b2 100644 --- a/miplearn/instance/picklegz.py +++ b/miplearn/instance/picklegz.py @@ -13,6 +13,7 @@ from overrides import overrides from miplearn.features.sample import Sample from miplearn.instance.base import Instance +from miplearn.types import ConstraintName, ConstraintCategory if TYPE_CHECKING: from miplearn.solvers.learning import InternalSolver @@ -58,19 +59,14 @@ class PickleGzInstance(Instance): return self.instance.get_variable_categories(names) @overrides - def get_constraint_features(self) -> Dict[str, List[float]]: + def get_constraint_features(self, names: np.ndarray) -> np.ndarray: assert self.instance is not None - return self.instance.get_constraint_features() + return self.instance.get_constraint_features(names) @overrides - def get_constraint_categories(self) -> Dict[str, str]: + def get_constraint_categories(self, names: np.ndarray) -> np.ndarray: assert self.instance is not None - return self.instance.get_constraint_categories() - - @overrides - def has_static_lazy_constraints(self) -> bool: - assert self.instance is not None - return self.instance.has_static_lazy_constraints() + return self.instance.get_constraint_categories(names) @overrides def has_dynamic_lazy_constraints(self) -> bool: @@ -78,16 +74,16 @@ class PickleGzInstance(Instance): return self.instance.has_dynamic_lazy_constraints() @overrides - def is_constraint_lazy(self, cid: str) -> bool: + def are_constraints_lazy(self, names: np.ndarray) -> np.ndarray: assert self.instance is not None - return self.instance.is_constraint_lazy(cid) + return self.instance.are_constraints_lazy(names) @overrides def find_violated_lazy_constraints( self, solver: "InternalSolver", model: Any, - ) -> List[str]: + ) -> List[ConstraintName]: assert self.instance is not None return self.instance.find_violated_lazy_constraints(solver, model) @@ -96,13 +92,13 @@ class PickleGzInstance(Instance): self, solver: "InternalSolver", model: Any, - violation: str, + violation: ConstraintName, ) -> None: assert self.instance is not None self.instance.enforce_lazy_constraint(solver, model, violation) @overrides - def find_violated_user_cuts(self, model: Any) -> List[str]: + def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]: assert self.instance is not None return self.instance.find_violated_user_cuts(model) @@ -111,7 +107,7 @@ class PickleGzInstance(Instance): self, solver: "InternalSolver", model: Any, - violation: str, + violation: ConstraintName, ) -> None: assert self.instance is not None self.instance.enforce_user_cut(solver, model, violation) diff --git a/miplearn/problems/tsp.py b/miplearn/problems/tsp.py index bdb053b..b277e3a 100644 --- a/miplearn/problems/tsp.py +++ b/miplearn/problems/tsp.py @@ -14,6 +14,7 @@ from scipy.stats.distributions import rv_frozen from miplearn.instance.base import Instance from miplearn.solvers.learning import InternalSolver from miplearn.solvers.pyomo.base import BasePyomoSolver +from miplearn.types import ConstraintName class ChallengeA: @@ -85,14 +86,14 @@ class TravelingSalesmanInstance(Instance): self, solver: InternalSolver, model: Any, - ) -> List[str]: + ) -> List[ConstraintName]: selected_edges = [e for e in self.edges if model.x[e].value > 0.5] graph = nx.Graph() graph.add_edges_from(selected_edges) violations = [] for c in list(nx.connected_components(graph)): if len(c) < self.n_cities: - violations.append(",".join(map(str, c))) + violations.append(",".join(map(str, c)).encode()) return violations @overrides @@ -100,10 +101,10 @@ class TravelingSalesmanInstance(Instance): self, solver: InternalSolver, model: Any, - violation: str, + violation: ConstraintName, ) -> None: assert isinstance(solver, BasePyomoSolver) - component = [int(v) for v in violation.split(",")] + component = [int(v) for v in violation.decode().split(",")] cut_edges = [ e for e in self.edges diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index 8caaffb..01ff2c2 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -260,7 +260,7 @@ def run_lazy_cb_tests(solver: InternalSolver) -> None: assert relsol is not None assert relsol[b"x[0]"] is not None if relsol[b"x[0]"] > 0: - instance.enforce_lazy_constraint(cb_solver, cb_model, "cut") + instance.enforce_lazy_constraint(cb_solver, cb_model, b"cut") solver.set_instance(instance, model) solver.solve(lazy_cb=lazy_cb) diff --git a/miplearn/types.py b/miplearn/types.py index 74a194e..2fd0345 100644 --- a/miplearn/types.py +++ b/miplearn/types.py @@ -11,6 +11,8 @@ if TYPE_CHECKING: from miplearn.solvers.learning import InternalSolver Category = bytes +ConstraintName = bytes +ConstraintCategory = bytes IterationCallback = Callable[[], bool] LazyCallback = Callable[[Any, Any], None] SolverParams = Dict[str, Any] diff --git a/tests/components/test_dynamic_lazy.py b/tests/components/test_dynamic_lazy.py index cec993e..e46c116 100644 --- a/tests/components/test_dynamic_lazy.py +++ b/tests/components/test_dynamic_lazy.py @@ -11,7 +11,7 @@ from miplearn.classifiers import Classifier from miplearn.classifiers.threshold import MinProbabilityThreshold from miplearn.components import classifier_evaluation_dict from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent -from miplearn.features.sample import Sample, MemorySample +from miplearn.features.sample import MemorySample from miplearn.instance.base import Instance from miplearn.solvers.tests import assert_equals @@ -24,71 +24,71 @@ def training_instances() -> List[Instance]: samples_0 = [ MemorySample( { - "mip_constr_lazy_enforced": {"c1", "c2"}, - "static_instance_features": [5.0], + "mip_constr_lazy_enforced": {b"c1", b"c2"}, + "static_instance_features": np.array([5.0]), }, ), MemorySample( { - "mip_constr_lazy_enforced": {"c2", "c3"}, - "static_instance_features": [5.0], + "mip_constr_lazy_enforced": {b"c2", b"c3"}, + "static_instance_features": np.array([5.0]), }, ), ] instances[0].get_samples = Mock(return_value=samples_0) # type: ignore instances[0].get_constraint_categories = Mock( # type: ignore - return_value={ - "c1": "type-a", - "c2": "type-a", - "c3": "type-b", - "c4": "type-b", - } + return_value=np.array(["type-a", "type-a", "type-b", "type-b"], dtype="S") ) instances[0].get_constraint_features = Mock( # type: ignore - return_value={ - "c1": [1.0, 2.0, 3.0], - "c2": [4.0, 5.0, 6.0], - "c3": [1.0, 2.0], - "c4": [3.0, 4.0], - } + return_value=np.array( + [ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [1.0, 2.0, 0.0], + [3.0, 4.0, 0.0], + ] + ) + ) + instances[0].are_constraints_lazy = Mock( # type: ignore + return_value=np.zeros(4, dtype=bool) ) samples_1 = [ MemorySample( { - "mip_constr_lazy_enforced": {"c3", "c4"}, - "static_instance_features": [8.0], + "mip_constr_lazy_enforced": {b"c3", b"c4"}, + "static_instance_features": np.array([8.0]), }, ) ] instances[1].get_samples = Mock(return_value=samples_1) # type: ignore instances[1].get_constraint_categories = Mock( # type: ignore - return_value={ - "c1": None, - "c2": "type-a", - "c3": "type-b", - "c4": "type-b", - } + return_value=np.array(["", "type-a", "type-b", "type-b"], dtype="S") ) instances[1].get_constraint_features = Mock( # type: ignore - return_value={ - "c2": [7.0, 8.0, 9.0], - "c3": [5.0, 6.0], - "c4": [7.0, 8.0], - } + return_value=np.array( + [ + [7.0, 8.0, 9.0], + [5.0, 6.0, 0.0], + [7.0, 8.0, 0.0], + ] + ) + ) + instances[1].are_constraints_lazy = Mock( # type: ignore + return_value=np.zeros(4, dtype=bool) ) return instances def test_sample_xy(training_instances: List[Instance]) -> None: comp = DynamicLazyConstraintsComponent() - comp.pre_fit([{"c1", "c2", "c3", "c4"}]) + comp.pre_fit([{b"c1", b"c2", b"c3", b"c4"}]) x_expected = { - "type-a": [[5.0, 1.0, 2.0, 3.0], [5.0, 4.0, 5.0, 6.0]], - "type-b": [[5.0, 1.0, 2.0], [5.0, 3.0, 4.0]], + b"type-a": np.array([[5.0, 1.0, 2.0, 3.0], [5.0, 4.0, 5.0, 6.0]]), + b"type-b": np.array([[5.0, 1.0, 2.0, 0.0], [5.0, 3.0, 4.0, 0.0]]), } y_expected = { - "type-a": [[False, True], [False, True]], - "type-b": [[True, False], [True, False]], + b"type-a": np.array([[False, True], [False, True]]), + b"type-b": np.array([[True, False], [True, False]]), } x_actual, y_actual = comp.sample_xy( training_instances[0], @@ -98,95 +98,26 @@ def test_sample_xy(training_instances: List[Instance]) -> None: assert_equals(y_actual, y_expected) -# def test_fit(training_instances: List[Instance]) -> None: -# clf = Mock(spec=Classifier) -# clf.clone = Mock(side_effect=lambda: Mock(spec=Classifier)) -# comp = DynamicLazyConstraintsComponent(classifier=clf) -# comp.fit(training_instances) -# assert clf.clone.call_count == 2 -# -# assert "type-a" in comp.classifiers -# clf_a = comp.classifiers["type-a"] -# assert clf_a.fit.call_count == 1 # type: ignore -# assert_array_equal( -# clf_a.fit.call_args[0][0], # type: ignore -# np.array( -# [ -# [5.0, 1.0, 2.0, 3.0], -# [5.0, 4.0, 5.0, 6.0], -# [5.0, 1.0, 2.0, 3.0], -# [5.0, 4.0, 5.0, 6.0], -# [8.0, 7.0, 8.0, 9.0], -# ] -# ), -# ) -# assert_array_equal( -# clf_a.fit.call_args[0][1], # type: ignore -# np.array( -# [ -# [False, True], -# [False, True], -# [True, False], -# [False, True], -# [True, False], -# ] -# ), -# ) -# -# assert "type-b" in comp.classifiers -# clf_b = comp.classifiers["type-b"] -# assert clf_b.fit.call_count == 1 # type: ignore -# assert_array_equal( -# clf_b.fit.call_args[0][0], # type: ignore -# np.array( -# [ -# [5.0, 1.0, 2.0], -# [5.0, 3.0, 4.0], -# [5.0, 1.0, 2.0], -# [5.0, 3.0, 4.0], -# [8.0, 5.0, 6.0], -# [8.0, 7.0, 8.0], -# ] -# ), -# ) -# assert_array_equal( -# clf_b.fit.call_args[0][1], # type: ignore -# np.array( -# [ -# [True, False], -# [True, False], -# [False, True], -# [True, False], -# [False, True], -# [False, True], -# ] -# ), -# ) - - def test_sample_predict_evaluate(training_instances: List[Instance]) -> None: comp = DynamicLazyConstraintsComponent() - comp.known_cids.extend(["c1", "c2", "c3", "c4"]) - comp.thresholds["type-a"] = MinProbabilityThreshold([0.5, 0.5]) - comp.thresholds["type-b"] = MinProbabilityThreshold([0.5, 0.5]) - comp.classifiers["type-a"] = Mock(spec=Classifier) - comp.classifiers["type-b"] = Mock(spec=Classifier) - comp.classifiers["type-a"].predict_proba = Mock( # type: ignore + comp.known_cids.extend([b"c1", b"c2", b"c3", b"c4"]) + comp.thresholds[b"type-a"] = MinProbabilityThreshold([0.5, 0.5]) + comp.thresholds[b"type-b"] = MinProbabilityThreshold([0.5, 0.5]) + comp.classifiers[b"type-a"] = Mock(spec=Classifier) + comp.classifiers[b"type-b"] = Mock(spec=Classifier) + comp.classifiers[b"type-a"].predict_proba = Mock( # type: ignore side_effect=lambda _: np.array([[0.1, 0.9], [0.8, 0.2]]) ) - comp.classifiers["type-b"].predict_proba = Mock( # type: ignore + comp.classifiers[b"type-b"].predict_proba = Mock( # type: ignore side_effect=lambda _: np.array([[0.9, 0.1], [0.1, 0.9]]) ) pred = comp.sample_predict( training_instances[0], training_instances[0].get_samples()[0], ) - assert pred == ["c1", "c4"] + assert pred == [b"c1", b"c4"] ev = comp.sample_evaluate( training_instances[0], training_instances[0].get_samples()[0], ) - assert ev == { - "type-a": classifier_evaluation_dict(tp=1, fp=0, tn=0, fn=1), - "type-b": classifier_evaluation_dict(tp=0, fp=1, tn=1, fn=0), - } + assert ev == classifier_evaluation_dict(tp=1, fp=1, tn=1, fn=1) diff --git a/tests/components/test_dynamic_user_cuts.py b/tests/components/test_dynamic_user_cuts.py index 44205a6..ab8e25c 100644 --- a/tests/components/test_dynamic_user_cuts.py +++ b/tests/components/test_dynamic_user_cuts.py @@ -17,6 +17,7 @@ from miplearn.components.dynamic_user_cuts import UserCutsComponent from miplearn.instance.base import Instance from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.learning import LearningSolver +from miplearn.types import ConstraintName, ConstraintCategory logger = logging.getLogger(__name__) @@ -40,13 +41,13 @@ class GurobiStableSetProblem(Instance): return True @overrides - def find_violated_user_cuts(self, model: Any) -> List[str]: + def find_violated_user_cuts(self, model: Any) -> List[ConstraintName]: assert isinstance(model, gp.Model) vals = model.cbGetNodeRel(model.getVars()) violations = [] for clique in nx.find_cliques(self.graph): if sum(vals[i] for i in clique) > 1: - violations.append(",".join([str(i) for i in clique])) + violations.append(",".join([str(i) for i in clique]).encode()) return violations @overrides @@ -54,9 +55,9 @@ class GurobiStableSetProblem(Instance): self, solver: InternalSolver, model: Any, - cid: str, + cid: ConstraintName, ) -> Any: - clique = [int(i) for i in cid.split(",")] + clique = [int(i) for i in cid.decode().split(",")] x = model.getVars() model.addConstr(gp.quicksum([x[i] for i in clique]) <= 1) diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index 8220244..cd09d16 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -17,6 +17,7 @@ from miplearn.solvers.internal import InternalSolver, Constraints from miplearn.solvers.learning import LearningSolver from miplearn.types import ( LearningSolveStats, + ConstraintCategory, ) @@ -25,11 +26,11 @@ def sample() -> Sample: sample = MemorySample( { "static_constr_categories": [ - "type-a", - "type-a", - "type-a", - "type-b", - "type-b", + b"type-a", + b"type-a", + b"type-a", + b"type-b", + b"type-b", ], "static_constr_lazy": np.array([True, True, True, True, False]), "static_constr_names": np.array(["c1", "c2", "c3", "c4", "c5"], dtype="S"), @@ -68,13 +69,13 @@ def test_usage_with_solver(instance: Instance) -> None: ) component = StaticLazyConstraintsComponent(violation_tolerance=1.0) - component.thresholds["type-a"] = MinProbabilityThreshold([0.5, 0.5]) - component.thresholds["type-b"] = MinProbabilityThreshold([0.5, 0.5]) + component.thresholds[b"type-a"] = MinProbabilityThreshold([0.5, 0.5]) + component.thresholds[b"type-b"] = MinProbabilityThreshold([0.5, 0.5]) component.classifiers = { - "type-a": Mock(spec=Classifier), - "type-b": Mock(spec=Classifier), + b"type-a": Mock(spec=Classifier), + b"type-b": Mock(spec=Classifier), } - component.classifiers["type-a"].predict_proba = Mock( # type: ignore + component.classifiers[b"type-a"].predict_proba = Mock( # type: ignore return_value=np.array( [ [0.00, 1.00], # c1 @@ -83,7 +84,7 @@ def test_usage_with_solver(instance: Instance) -> None: ] ) ) - component.classifiers["type-b"].predict_proba = Mock( # type: ignore + component.classifiers[b"type-b"].predict_proba = Mock( # type: ignore return_value=np.array( [ [0.02, 0.98], # c4 @@ -105,8 +106,8 @@ def test_usage_with_solver(instance: Instance) -> None: ) # Should ask ML to predict whether each lazy constraint should be enforced - component.classifiers["type-a"].predict_proba.assert_called_once() - component.classifiers["type-b"].predict_proba.assert_called_once() + component.classifiers[b"type-a"].predict_proba.assert_called_once() + component.classifiers[b"type-b"].predict_proba.assert_called_once() # Should ask internal solver to remove some constraints assert internal.remove_constraints.call_count == 1 @@ -153,18 +154,18 @@ def test_usage_with_solver(instance: Instance) -> None: def test_sample_predict(sample: Sample) -> None: comp = StaticLazyConstraintsComponent() - comp.thresholds["type-a"] = MinProbabilityThreshold([0.5, 0.5]) - comp.thresholds["type-b"] = MinProbabilityThreshold([0.5, 0.5]) - comp.classifiers["type-a"] = Mock(spec=Classifier) - comp.classifiers["type-a"].predict_proba = lambda _: np.array( # type:ignore + comp.thresholds[b"type-a"] = MinProbabilityThreshold([0.5, 0.5]) + comp.thresholds[b"type-b"] = MinProbabilityThreshold([0.5, 0.5]) + comp.classifiers[b"type-a"] = Mock(spec=Classifier) + comp.classifiers[b"type-a"].predict_proba = lambda _: np.array( # type:ignore [ [0.0, 1.0], # c1 [0.0, 0.9], # c2 [0.9, 0.1], # c3 ] ) - comp.classifiers["type-b"] = Mock(spec=Classifier) - comp.classifiers["type-b"].predict_proba = lambda _: np.array( # type:ignore + comp.classifiers[b"type-b"] = Mock(spec=Classifier) + comp.classifiers[b"type-b"].predict_proba = lambda _: np.array( # type:ignore [ [0.0, 1.0], # c4 ] @@ -175,17 +176,17 @@ def test_sample_predict(sample: Sample) -> None: def test_fit_xy() -> None: x = cast( - Dict[str, np.ndarray], + Dict[ConstraintCategory, np.ndarray], { - "type-a": np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]]), - "type-b": np.array([[1.0, 4.0, 0.0]]), + b"type-a": np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]]), + b"type-b": np.array([[1.0, 4.0, 0.0]]), }, ) y = cast( - Dict[str, np.ndarray], + Dict[ConstraintCategory, np.ndarray], { - "type-a": np.array([[False, True], [False, True], [True, False]]), - "type-b": np.array([[False, True]]), + b"type-a": np.array([[False, True], [False, True], [True, False]]), + b"type-b": np.array([[False, True]]), }, ) clf: Classifier = Mock(spec=Classifier) @@ -198,15 +199,15 @@ def test_fit_xy() -> None: ) comp.fit_xy(x, y) assert clf.clone.call_count == 2 - clf_a = comp.classifiers["type-a"] - clf_b = comp.classifiers["type-b"] + clf_a = comp.classifiers[b"type-a"] + clf_b = comp.classifiers[b"type-b"] assert clf_a.fit.call_count == 1 # type: ignore assert clf_b.fit.call_count == 1 # type: ignore - assert_array_equal(clf_a.fit.call_args[0][0], x["type-a"]) # type: ignore - assert_array_equal(clf_b.fit.call_args[0][0], x["type-b"]) # type: ignore + assert_array_equal(clf_a.fit.call_args[0][0], x[b"type-a"]) # type: ignore + assert_array_equal(clf_b.fit.call_args[0][0], x[b"type-b"]) # type: ignore assert thr.clone.call_count == 2 - thr_a = comp.thresholds["type-a"] - thr_b = comp.thresholds["type-b"] + thr_a = comp.thresholds[b"type-a"] + thr_b = comp.thresholds[b"type-b"] assert thr_a.fit.call_count == 1 # type: ignore assert thr_b.fit.call_count == 1 # type: ignore assert thr_a.fit.call_args[0][0] == clf_a # type: ignore @@ -215,12 +216,12 @@ def test_fit_xy() -> None: def test_sample_xy(sample: Sample) -> None: x_expected = { - "type-a": [[5.0, 1.0, 1.0], [5.0, 1.0, 2.0], [5.0, 1.0, 3.0]], - "type-b": [[5.0, 1.0, 4.0, 0.0]], + b"type-a": [[5.0, 1.0, 1.0], [5.0, 1.0, 2.0], [5.0, 1.0, 3.0]], + b"type-b": [[5.0, 1.0, 4.0, 0.0]], } y_expected = { - "type-a": [[False, True], [False, True], [True, False]], - "type-b": [[False, True]], + b"type-a": [[False, True], [False, True], [True, False]], + b"type-b": [[False, True]], } xy = StaticLazyConstraintsComponent().sample_xy(None, sample) assert xy is not None diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 9d0da22..732aaa1 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -32,27 +32,45 @@ def test_knapsack() -> None: # ------------------------------------------------------- extractor.extract_after_load_features(instance, solver, sample) assert_equals( - sample.get_vector("static_var_names"), + sample.get_array("static_instance_features"), + np.array([67.0, 21.75]), + ) + assert_equals( + sample.get_array("static_var_names"), np.array(["x[0]", "x[1]", "x[2]", "x[3]", "z"], dtype="S"), ) assert_equals( - sample.get_vector("static_var_lower_bounds"), [0.0, 0.0, 0.0, 0.0, 0.0] + sample.get_array("static_var_lower_bounds"), + np.array([0.0, 0.0, 0.0, 0.0, 0.0]), ) assert_equals( - sample.get_array("static_var_obj_coeffs"), [505.0, 352.0, 458.0, 220.0, 0.0] + sample.get_array("static_var_obj_coeffs"), + np.array([505.0, 352.0, 458.0, 220.0, 0.0]), ) assert_equals( sample.get_array("static_var_types"), np.array(["B", "B", "B", "B", "C"], dtype="S"), ) assert_equals( - sample.get_vector("static_var_upper_bounds"), [1.0, 1.0, 1.0, 1.0, 67.0] + sample.get_array("static_var_upper_bounds"), + np.array([1.0, 1.0, 1.0, 1.0, 67.0]), ) assert_equals( sample.get_array("static_var_categories"), np.array(["default", "default", "default", "default", ""], dtype="S"), ) - assert sample.get_vector_list("static_var_features") is not None + assert_equals( + sample.get_vector_list("static_var_features"), + np.array( + [ + [23.0, 505.0, 1.0, 0.32899, 0.0, 0.0, 505.0, 1.0], + [26.0, 352.0, 1.0, 0.229316, 0.0, 0.0, 352.0, 1.0], + [20.0, 458.0, 1.0, 0.298371, 0.0, 0.0, 458.0, 1.0], + [18.0, 220.0, 1.0, 0.143322, 0.0, 0.0, 220.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 67.0], + ] + ), + ) assert_equals( sample.get_array("static_constr_names"), np.array(["eq_capacity"], dtype="S"), @@ -69,18 +87,30 @@ def test_knapsack() -> None: # ], # ], # ) - assert_equals(sample.get_vector("static_constr_rhs"), [0.0]) + assert_equals( + sample.get_vector("static_constr_rhs"), + np.array([0.0]), + ) assert_equals( sample.get_array("static_constr_senses"), np.array(["="], dtype="S"), ) - assert_equals(sample.get_vector("static_constr_features"), [None]) + assert_equals( + sample.get_vector("static_constr_features"), + np.array([[0.0]]), + ) assert_equals( sample.get_vector("static_constr_categories"), np.array(["eq_capacity"], dtype="S"), ) - assert_equals(sample.get_array("static_constr_lazy"), np.array([False])) - assert_equals(sample.get_vector("static_instance_features"), [67.0, 21.75]) + assert_equals( + sample.get_array("static_constr_lazy"), + np.array([False]), + ) + assert_equals( + sample.get_vector("static_instance_features"), + np.array([67.0, 21.75]), + ) assert_equals(sample.get_scalar("static_constr_lazy_count"), 0) # after-lp @@ -112,26 +142,187 @@ def test_knapsack() -> None: [inf, 570.869565, inf, 243.692308, inf], ) assert_equals( - sample.get_array("lp_var_sa_ub_down"), [0.913043, 0.923077, 0.9, 0.0, 43.0] + sample.get_array("lp_var_sa_ub_down"), + np.array([0.913043, 0.923077, 0.9, 0.0, 43.0]), + ) + assert_equals( + sample.get_array("lp_var_sa_ub_up"), + np.array([2.043478, inf, 2.2, inf, 69.0]), + ) + assert_equals( + sample.get_array("lp_var_values"), + np.array([1.0, 0.923077, 1.0, 0.0, 67.0]), + ) + assert_equals( + sample.get_vector_list("lp_var_features"), + np.array( + [ + [ + 23.0, + 505.0, + 1.0, + 0.32899, + 0.0, + 0.0, + 505.0, + 1.0, + 1.0, + 0.32899, + 0.0, + 0.0, + 1.0, + 1.0, + 5.265874, + 46.051702, + 193.615385, + -inf, + 1.0, + 311.384615, + inf, + 0.913043, + 2.043478, + 1.0, + ], + [ + 26.0, + 352.0, + 1.0, + 0.229316, + 0.0, + 0.0, + 352.0, + 1.0, + 1.0, + 0.229316, + 0.0, + 0.076923, + 1.0, + 1.0, + 3.532875, + 5.388476, + 0.0, + -inf, + 0.923077, + 317.777778, + 570.869565, + 0.923077, + inf, + 0.923077, + ], + [ + 20.0, + 458.0, + 1.0, + 0.298371, + 0.0, + 0.0, + 458.0, + 1.0, + 1.0, + 0.298371, + 0.0, + 0.0, + 1.0, + 1.0, + 5.232342, + 46.051702, + 187.230769, + -inf, + 1.0, + 270.769231, + inf, + 0.9, + 2.2, + 1.0, + ], + [ + 18.0, + 220.0, + 1.0, + 0.143322, + 0.0, + 0.0, + 220.0, + 1.0, + 1.0, + 0.143322, + 0.0, + 0.0, + 1.0, + -1.0, + 46.051702, + 3.16515, + -23.692308, + -0.111111, + 1.0, + -inf, + 243.692308, + 0.0, + inf, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 67.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + -1.0, + 0.0, + 0.0, + 13.538462, + -inf, + 67.0, + -13.538462, + inf, + 43.0, + 69.0, + 67.0, + ], + ] + ), ) - assert_equals(sample.get_array("lp_var_sa_ub_up"), [2.043478, inf, 2.2, inf, 69.0]) - assert_equals(sample.get_array("lp_var_values"), [1.0, 0.923077, 1.0, 0.0, 67.0]) - assert sample.get_vector_list("lp_var_features") is not None assert_equals( sample.get_array("lp_constr_basis_status"), np.array(["N"], dtype="S"), ) - assert_equals(sample.get_array("lp_constr_dual_values"), [13.538462]) - assert_equals(sample.get_array("lp_constr_sa_rhs_down"), [-24.0]) - assert_equals(sample.get_array("lp_constr_sa_rhs_up"), [2.0]) - assert_equals(sample.get_array("lp_constr_slacks"), [0.0]) + assert_equals( + sample.get_array("lp_constr_dual_values"), + np.array([13.538462]), + ) + assert_equals( + sample.get_array("lp_constr_sa_rhs_down"), + np.array([-24.0]), + ) + assert_equals( + sample.get_array("lp_constr_sa_rhs_up"), + np.array([2.0]), + ) + assert_equals( + sample.get_array("lp_constr_slacks"), + np.array([0.0]), + ) + assert_equals( + sample.get_array("lp_constr_features"), + np.array([[0.0, 13.538462, -24.0, 2.0, 0.0]]), + ) # after-mip # ------------------------------------------------------- solver.solve() extractor.extract_after_mip_features(solver, sample) - assert_equals(sample.get_array("mip_var_values"), [1.0, 0.0, 1.0, 1.0, 61.0]) - assert_equals(sample.get_array("mip_constr_slacks"), [0.0]) + assert_equals( + sample.get_array("mip_var_values"), np.array([1.0, 0.0, 1.0, 1.0, 61.0]) + ) + assert_equals(sample.get_array("mip_constr_slacks"), np.array([0.0])) def test_constraint_getindex() -> None: @@ -200,7 +391,7 @@ class MpsInstance(Instance): return gp.read(self.filename) -if __name__ == "__main__": +def main() -> None: solver = GurobiSolver() instance = MpsInstance(sys.argv[1]) solver.set_instance(instance) @@ -213,3 +404,7 @@ if __name__ == "__main__": extractor.extract_after_lp_features(solver, sample, lp_stats) cProfile.run("run()", filename="tmp/prof") + + +if __name__ == "__main__": + main() From 60b9a6775fbc68d8214886ea20a71001b50a8262 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Tue, 10 Aug 2021 10:28:30 -0500 Subject: [PATCH 56/67] Use NumPy to compute AlvLouWeh2017 features --- miplearn/features/extractor.py | 148 +++++++++++++++++-------------- tests/features/test_extractor.py | 60 +++++-------- 2 files changed, 105 insertions(+), 103 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 14fa673..155c153 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -127,7 +127,9 @@ class FeaturesExtractor: ]: if f is not None: lp_var_features_list.append(f.reshape(-1, 1)) - sample.put_array("lp_var_features", np.hstack(lp_var_features_list)) + lp_var_features = np.hstack(lp_var_features_list) + _fix_infinity(lp_var_features) + sample.put_array("lp_var_features", lp_var_features) # Constraint features lp_constr_features_list = [] @@ -142,7 +144,9 @@ class FeaturesExtractor: ]: if f is not None: lp_constr_features_list.append(f.reshape(-1, 1)) - sample.put_array("lp_constr_features", np.hstack(lp_constr_features_list)) + lp_constr_features = np.hstack(lp_constr_features_list) + _fix_infinity(lp_constr_features) + sample.put_array("lp_constr_features", lp_constr_features) # Build lp_instance_features static_instance_features = sample.get_array("static_instance_features") @@ -311,73 +315,83 @@ class FeaturesExtractor: obj_sa_down = sample.get_array("lp_var_sa_obj_down") obj_sa_up = sample.get_array("lp_var_sa_obj_up") values = sample.get_array("lp_var_values") - assert obj_coeffs is not None - - pos_obj_coeff_sum = 0.0 - neg_obj_coeff_sum = 0.0 - for coeff in obj_coeffs: - if coeff > 0: - pos_obj_coeff_sum += coeff - if coeff < 0: - neg_obj_coeff_sum += -coeff - - features = [] - for i in range(len(obj_coeffs)): - f: List[float] = [] - if obj_coeffs is not None: - # Feature 1 - f.append(np.sign(obj_coeffs[i])) - - # Feature 2 - if pos_obj_coeff_sum > 0: - f.append(abs(obj_coeffs[i]) / pos_obj_coeff_sum) - else: - f.append(0.0) - - # Feature 3 - if neg_obj_coeff_sum > 0: - f.append(abs(obj_coeffs[i]) / neg_obj_coeff_sum) - else: - f.append(0.0) + assert obj_coeffs is not None + obj_coeffs = obj_coeffs.astype(float) + _fix_infinity(obj_coeffs) + nvars = len(obj_coeffs) + + if obj_sa_down is not None: + obj_sa_down = obj_sa_down.astype(float) + _fix_infinity(obj_sa_down) + + if obj_sa_up is not None: + obj_sa_up = obj_sa_up.astype(float) + _fix_infinity(obj_sa_up) + + if values is not None: + values = values.astype(float) + _fix_infinity(values) + + pos_obj_coeffs_sum = obj_coeffs[obj_coeffs > 0].sum() + neg_obj_coeffs_sum = -obj_coeffs[obj_coeffs < 0].sum() + + curr = 0 + max_n_features = 8 + features = np.zeros((nvars, max_n_features)) + with np.errstate(divide="ignore", invalid="ignore"): + # Feature 1 + features[:, curr] = np.sign(obj_coeffs) + curr += 1 + + # Feature 2 + if abs(pos_obj_coeffs_sum) > 0: + features[:, curr] = np.abs(obj_coeffs) / pos_obj_coeffs_sum + curr += 1 + + # Feature 3 + if abs(neg_obj_coeffs_sum) > 0: + features[:, curr] = np.abs(obj_coeffs) / neg_obj_coeffs_sum + curr += 1 + + # Feature 37 if values is not None: - # Feature 37 - f.append( - min( - values[i] - np.floor(values[i]), - np.ceil(values[i]) - values[i], - ) + features[:, curr] = np.minimum( + values - np.floor(values), + np.ceil(values) - values, ) + curr += 1 + # Feature 44 if obj_sa_up is not None: - assert obj_sa_down is not None - assert obj_coeffs is not None - - # Convert inf into large finite numbers - sd = max(-1e20, obj_sa_down[i]) - su = min(1e20, obj_sa_up[i]) - obj = obj_coeffs[i] - - # Features 44 and 46 - f.append(np.sign(obj_sa_up[i])) - f.append(np.sign(obj_sa_down[i])) - - # Feature 47 - csign = np.sign(obj) - if csign != 0 and ((obj - sd) / csign) > 0.001: - f.append(log((obj - sd) / csign)) - else: - f.append(0.0) - - # Feature 48 - if csign != 0 and ((su - obj) / csign) > 0.001: - f.append(log((su - obj) / csign)) - else: - f.append(0.0) - - for (i, v) in enumerate(f): - if not isfinite(v): - f[i] = 0.0 - - features.append(f) - return np.array(features, dtype=float) + features[:, curr] = np.sign(obj_sa_up) + curr += 1 + + # Feature 46 + if obj_sa_down is not None: + features[:, curr] = np.sign(obj_sa_down) + curr += 1 + + # Feature 47 + if obj_sa_down is not None: + features[:, curr] = np.log( + obj_coeffs - obj_sa_down / np.sign(obj_coeffs) + ) + curr += 1 + + # Feature 48 + if obj_sa_up is not None: + features[:, curr] = np.log(obj_coeffs - obj_sa_up / np.sign(obj_coeffs)) + curr += 1 + + features = features[:, 0:curr] + _fix_infinity(features) + return features + + +def _fix_infinity(m: np.ndarray) -> None: + masked = np.ma.masked_invalid(m) + max_values = np.max(masked, axis=0) + min_values = np.min(masked, axis=0) + m[:] = np.maximum(np.minimum(m, max_values), min_values) + m[np.isnan(m)] = 0.0 diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 732aaa1..2c6ac37 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -2,6 +2,7 @@ # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. +import os import sys import time from typing import Any @@ -63,11 +64,11 @@ def test_knapsack() -> None: sample.get_vector_list("static_var_features"), np.array( [ - [23.0, 505.0, 1.0, 0.32899, 0.0, 0.0, 505.0, 1.0], - [26.0, 352.0, 1.0, 0.229316, 0.0, 0.0, 352.0, 1.0], - [20.0, 458.0, 1.0, 0.298371, 0.0, 0.0, 458.0, 1.0], - [18.0, 220.0, 1.0, 0.143322, 0.0, 0.0, 220.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 67.0], + [23.0, 505.0, 1.0, 0.32899, 0.0, 505.0, 1.0], + [26.0, 352.0, 1.0, 0.229316, 0.0, 352.0, 1.0], + [20.0, 458.0, 1.0, 0.298371, 0.0, 458.0, 1.0], + [18.0, 220.0, 1.0, 0.143322, 0.0, 220.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 67.0], ] ), ) @@ -163,22 +164,20 @@ def test_knapsack() -> None: 1.0, 0.32899, 0.0, - 0.0, 505.0, 1.0, 1.0, 0.32899, 0.0, - 0.0, 1.0, 1.0, 5.265874, - 46.051702, + 0.0, 193.615385, - -inf, + -0.111111, 1.0, 311.384615, - inf, + 570.869565, 0.913043, 2.043478, 1.0, @@ -189,24 +188,22 @@ def test_knapsack() -> None: 1.0, 0.229316, 0.0, - 0.0, 352.0, 1.0, 1.0, 0.229316, - 0.0, 0.076923, 1.0, 1.0, 3.532875, - 5.388476, 0.0, - -inf, + 0.0, + -0.111111, 0.923077, 317.777778, 570.869565, 0.923077, - inf, + 69.0, 0.923077, ], [ @@ -215,22 +212,20 @@ def test_knapsack() -> None: 1.0, 0.298371, 0.0, - 0.0, 458.0, 1.0, 1.0, 0.298371, 0.0, - 0.0, 1.0, 1.0, 5.232342, - 46.051702, + 0.0, 187.230769, - -inf, + -0.111111, 1.0, 270.769231, - inf, + 570.869565, 0.9, 2.2, 1.0, @@ -241,24 +236,22 @@ def test_knapsack() -> None: 1.0, 0.143322, 0.0, - 0.0, 220.0, 1.0, 1.0, 0.143322, 0.0, - 0.0, 1.0, -1.0, - 46.051702, - 3.16515, + 5.453347, + 0.0, -23.692308, -0.111111, 1.0, - -inf, + -13.538462, 243.692308, 0.0, - inf, + 69.0, 0.0, ], [ @@ -268,21 +261,19 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, - 0.0, 67.0, 0.0, 0.0, 0.0, - 0.0, 1.0, -1.0, - 0.0, + 5.453347, 0.0, 13.538462, - -inf, + -0.111111, 67.0, -13.538462, - inf, + 570.869565, 43.0, 69.0, 67.0, @@ -391,7 +382,7 @@ class MpsInstance(Instance): return gp.read(self.filename) -def main() -> None: +if __name__ == "__main__": solver = GurobiSolver() instance = MpsInstance(sys.argv[1]) solver.set_instance(instance) @@ -404,7 +395,4 @@ def main() -> None: extractor.extract_after_lp_features(solver, sample, lp_stats) cProfile.run("run()", filename="tmp/prof") - - -if __name__ == "__main__": - main() + os.system("flameprof tmp/prof > tmp/prof.svg") From ed58242b5c9da70cfc7dd0a0b91b4f8cbb259fe2 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 10 Aug 2021 11:52:02 -0500 Subject: [PATCH 57/67] Remove most usages of put_{vector,vector_list}; deprecate get_set --- miplearn/components/objective.py | 11 +- miplearn/components/primal.py | 6 +- miplearn/components/static_lazy.py | 10 +- miplearn/features/sample.py | 144 +-------------------------- miplearn/instance/file.py | 6 +- tests/components/test_objective.py | 15 +-- tests/components/test_static_lazy.py | 18 ++-- tests/features/test_extractor.py | 4 +- tests/instance/test_file.py | 2 +- 9 files changed, 40 insertions(+), 176 deletions(-) diff --git a/miplearn/components/objective.py b/miplearn/components/objective.py index cc4e30c..af1316f 100644 --- a/miplearn/components/objective.py +++ b/miplearn/components/objective.py @@ -3,7 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging -from typing import List, Dict, Any, TYPE_CHECKING, Tuple, Optional +from typing import List, Dict, Any, TYPE_CHECKING, Tuple, Optional, cast import numpy as np from overrides import overrides @@ -77,10 +77,11 @@ class ObjectiveValueComponent(Component): _: Optional[Instance], sample: Sample, ) -> Tuple[Dict[str, List[List[float]]], Dict[str, List[List[float]]]]: - lp_instance_features = sample.get_vector("lp_instance_features") - if lp_instance_features is None: - lp_instance_features = sample.get_vector("static_instance_features") - assert lp_instance_features is not None + lp_instance_features_np = sample.get_array("lp_instance_features") + if lp_instance_features_np is None: + lp_instance_features_np = sample.get_array("static_instance_features") + assert lp_instance_features_np is not None + lp_instance_features = cast(List[float], lp_instance_features_np.tolist()) # Features x: Dict[str, List[List[float]]] = { diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index 769cbef..00a0140 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -142,13 +142,13 @@ class PrimalSolutionComponent(Component): ) -> Tuple[Dict[Category, List[List[float]]], Dict[Category, List[List[float]]]]: x: Dict = {} y: Dict = {} - instance_features = sample.get_vector("static_instance_features") + instance_features = sample.get_array("static_instance_features") mip_var_values = sample.get_array("mip_var_values") - var_features = sample.get_vector_list("lp_var_features") + var_features = sample.get_array("lp_var_features") var_names = sample.get_array("static_var_names") var_categories = sample.get_array("static_var_categories") if var_features is None: - var_features = sample.get_vector_list("static_var_features") + var_features = sample.get_array("static_var_features") assert instance_features is not None assert var_features is not None assert var_names is not None diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index efc11d8..2dd300e 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -207,14 +207,14 @@ class StaticLazyConstraintsComponent(Component): x: Dict[ConstraintCategory, List[List[float]]] = {} y: Dict[ConstraintCategory, List[List[float]]] = {} cids: Dict[ConstraintCategory, List[ConstraintName]] = {} - instance_features = sample.get_vector("static_instance_features") - constr_features = sample.get_vector_list("lp_constr_features") + instance_features = sample.get_array("static_instance_features") + constr_features = sample.get_array("lp_constr_features") constr_names = sample.get_array("static_constr_names") - constr_categories = sample.get_vector("static_constr_categories") + constr_categories = sample.get_array("static_constr_categories") constr_lazy = sample.get_array("static_constr_lazy") lazy_enforced = sample.get_set("mip_constr_lazy_enforced") if constr_features is None: - constr_features = sample.get_vector_list("static_constr_features") + constr_features = sample.get_array("static_constr_features") assert instance_features is not None assert constr_features is not None @@ -227,7 +227,7 @@ class StaticLazyConstraintsComponent(Component): if not constr_lazy[cidx]: continue category = constr_categories[cidx] - if category is None: + if len(category) == 0: continue if category not in x: x[category] = [] diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index d3b8d65..6afde9e 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -38,15 +38,6 @@ VectorList = Union[ class Sample(ABC): """Abstract dictionary-like class that stores training data.""" - @abstractmethod - def get_bytes(self, key: str) -> Optional[Bytes]: - warnings.warn("Deprecated", DeprecationWarning) - return None - - @abstractmethod - def put_bytes(self, key: str, value: Bytes) -> None: - warnings.warn("Deprecated", DeprecationWarning) - @abstractmethod def get_scalar(self, key: str) -> Optional[Any]: pass @@ -64,15 +55,6 @@ class Sample(ABC): def put_vector(self, key: str, value: Vector) -> None: warnings.warn("Deprecated", DeprecationWarning) - @abstractmethod - def get_vector_list(self, key: str) -> Optional[Any]: - warnings.warn("Deprecated", DeprecationWarning) - return None - - @abstractmethod - def put_vector_list(self, key: str, value: VectorList) -> None: - warnings.warn("Deprecated", DeprecationWarning) - @abstractmethod def put_array(self, key: str, value: Optional[np.ndarray]) -> None: pass @@ -90,6 +72,7 @@ class Sample(ABC): pass def get_set(self, key: str) -> Set: + warnings.warn("Deprecated", DeprecationWarning) v = self.get_vector(key) if v: return set(v) @@ -97,6 +80,7 @@ class Sample(ABC): return set() def put_set(self, key: str, value: Set) -> None: + warnings.warn("Deprecated", DeprecationWarning) v = list(value) self.put_vector(key, v) @@ -114,15 +98,6 @@ class Sample(ABC): for v in value: self._assert_is_scalar(v) - def _assert_is_vector_list(self, value: Any) -> None: - assert isinstance( - value, (list, np.ndarray) - ), f"list or numpy array expected; found instead: {value} ({value.__class__})" - for v in value: - if v is None: - continue - self._assert_is_vector(v) - def _assert_supported(self, value: np.ndarray) -> None: assert isinstance(value, np.ndarray) assert value.dtype.kind in "biufS", f"Unsupported dtype: {value.dtype}" @@ -145,10 +120,6 @@ class MemorySample(Sample): self._data: Dict[str, Any] = data self._check_data = check_data - @overrides - def get_bytes(self, key: str) -> Optional[Bytes]: - return self._get(key) - @overrides def get_scalar(self, key: str) -> Optional[Any]: return self._get(key) @@ -157,17 +128,6 @@ class MemorySample(Sample): def get_vector(self, key: str) -> Optional[Any]: return self._get(key) - @overrides - def get_vector_list(self, key: str) -> Optional[Any]: - return self._get(key) - - @overrides - def put_bytes(self, key: str, value: Bytes) -> None: - assert isinstance( - value, (bytes, bytearray) - ), f"bytes expected; found: {value}" # type: ignore - self._put(key, value) - @overrides def put_scalar(self, key: str, value: Scalar) -> None: if value is None: @@ -184,12 +144,6 @@ class MemorySample(Sample): self._assert_is_vector(value) self._put(key, value) - @overrides - def put_vector_list(self, key: str, value: VectorList) -> None: - if self._check_data: - self._assert_is_vector_list(value) - self._put(key, value) - def _get(self, key: str) -> Optional[Any]: if key in self._data: return self._data[key] @@ -239,16 +193,6 @@ class Hdf5Sample(Sample): self.file = h5py.File(filename, mode, libver="latest") self._check_data = check_data - @overrides - def get_bytes(self, key: str) -> Optional[Bytes]: - if key not in self.file: - return None - ds = self.file[key] - assert ( - len(ds.shape) == 1 - ), f"1-dimensional array expected; found shape {ds.shape}" - return ds[()].tobytes() - @overrides def get_scalar(self, key: str) -> Optional[Any]: if key not in self.file: @@ -277,26 +221,6 @@ class Hdf5Sample(Sample): else: return ds[:].tolist() - @overrides - def get_vector_list(self, key: str) -> Optional[Any]: - if key not in self.file: - return None - ds = self.file[key] - lens = self.get_vector(f"{key}_lengths") - if h5py.check_string_dtype(ds.dtype): - padded = ds.asstr()[:].tolist() - else: - padded = ds[:].tolist() - return _crop(padded, lens) - - @overrides - def put_bytes(self, key: str, value: Bytes) -> None: - if self._check_data: - assert isinstance( - value, (bytes, bytearray) - ), f"bytes expected; found: {value}" # type: ignore - self._put(key, np.frombuffer(value, dtype="uint8"), compress=True) - @overrides def put_scalar(self, key: str, value: Any) -> None: if value is None: @@ -328,29 +252,6 @@ class Hdf5Sample(Sample): self._put(key, value, compress=True) - @overrides - def put_vector_list(self, key: str, value: VectorList) -> None: - if self._check_data: - self._assert_is_vector_list(value) - padded, lens = _pad(value) - self.put_vector(f"{key}_lengths", lens) - data = None - for v in value: - if v is None or len(v) == 0: - continue - if isinstance(v[0], str): - data = np.array(padded, dtype="S") - elif isinstance(v[0], float): - data = np.array(padded, dtype=np.dtype("f2")) - elif isinstance(v[0], bool): - data = np.array(padded, dtype=bool) - else: - data = np.array(padded) - break - if data is None: - data = np.array(padded) - self._put(key, data, compress=True) - def _put(self, key: str, value: Any, compress: bool = False) -> Dataset: if key in self.file: del self.file[key] @@ -394,44 +295,3 @@ class Hdf5Sample(Sample): assert col is not None assert data is not None return coo_matrix((data, (row, col))) - - -def _pad(veclist: VectorList) -> Tuple[VectorList, List[int]]: - veclist = deepcopy(veclist) - lens = [len(v) if v is not None else -1 for v in veclist] - maxlen = max(lens) - - # Find appropriate constant to pad the vectors - constant: Union[int, float, str] = 0 - for v in veclist: - if v is None or len(v) == 0: - continue - if isinstance(v[0], int): - constant = 0 - elif isinstance(v[0], float): - constant = 0.0 - elif isinstance(v[0], str): - constant = "" - else: - assert False, f"unsupported data type: {v[0]}" - - # Pad vectors - for (i, vi) in enumerate(veclist): - if vi is None: - vi = veclist[i] = [] - assert isinstance(vi, list), f"list expected; found: {vi}" - for k in range(len(vi), maxlen): - vi.append(constant) - - return veclist, lens - - -def _crop(veclist: VectorList, lens: List[int]) -> VectorList: - result: VectorList = cast(VectorList, []) - for (i, v) in enumerate(veclist): - if lens[i] < 0: - result.append(None) # type: ignore - else: - assert isinstance(v, list) - result.append(v[: lens[i]]) - return result diff --git a/miplearn/instance/file.py b/miplearn/instance/file.py index 5c2615f..a08d6b2 100644 --- a/miplearn/instance/file.py +++ b/miplearn/instance/file.py @@ -111,14 +111,14 @@ class FileInstance(Instance): def load(self) -> None: if self.instance is not None: return - self.instance = pickle.loads(self.h5.get_bytes("pickled")) + self.instance = pickle.loads(self.h5.get_array("pickled").tobytes()) assert isinstance(self.instance, Instance) @classmethod def save(cls, instance: Instance, filename: str) -> None: h5 = Hdf5Sample(filename, mode="w") - instance_pkl = pickle.dumps(instance) - h5.put_bytes("pickled", instance_pkl) + instance_pkl = np.frombuffer(pickle.dumps(instance), dtype=np.int8) + h5.put_array("pickled", instance_pkl) @overrides def create_sample(self) -> Sample: diff --git a/tests/components/test_objective.py b/tests/components/test_objective.py index 82a6a05..fc45083 100644 --- a/tests/components/test_objective.py +++ b/tests/components/test_objective.py @@ -13,6 +13,7 @@ from miplearn.components.objective import ObjectiveValueComponent from miplearn.features.sample import Sample, MemorySample from miplearn.solvers.learning import LearningSolver from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver +from miplearn.solvers.tests import assert_equals @pytest.fixture @@ -21,7 +22,7 @@ def sample() -> Sample: { "mip_lower_bound": 1.0, "mip_upper_bound": 2.0, - "lp_instance_features": [1.0, 2.0, 3.0], + "lp_instance_features": np.array([1.0, 2.0, 3.0]), }, ) return sample @@ -29,18 +30,18 @@ def sample() -> Sample: def test_sample_xy(sample: Sample) -> None: x_expected = { - "Lower bound": [[1.0, 2.0, 3.0]], - "Upper bound": [[1.0, 2.0, 3.0]], + "Lower bound": np.array([[1.0, 2.0, 3.0]]), + "Upper bound": np.array([[1.0, 2.0, 3.0]]), } y_expected = { - "Lower bound": [[1.0]], - "Upper bound": [[2.0]], + "Lower bound": np.array([[1.0]]), + "Upper bound": np.array([[2.0]]), } xy = ObjectiveValueComponent().sample_xy(None, sample) assert xy is not None x_actual, y_actual = xy - assert x_actual == x_expected - assert y_actual == y_expected + assert_equals(x_actual, x_expected) + assert_equals(y_actual, y_expected) def test_fit_xy() -> None: diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index cd09d16..5a38822 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -36,13 +36,15 @@ def sample() -> Sample: "static_constr_names": np.array(["c1", "c2", "c3", "c4", "c5"], dtype="S"), "static_instance_features": [5.0], "mip_constr_lazy_enforced": {b"c1", b"c2", b"c4"}, - "lp_constr_features": [ - [1.0, 1.0], - [1.0, 2.0], - [1.0, 3.0], - [1.0, 4.0, 0.0], - None, - ], + "lp_constr_features": np.array( + [ + [1.0, 1.0, 0.0], + [1.0, 2.0, 0.0], + [1.0, 3.0, 0.0], + [1.0, 4.0, 0.0], + [0.0, 0.0, 0.0], + ] + ), "static_constr_lazy_count": 4, }, ) @@ -216,7 +218,7 @@ def test_fit_xy() -> None: def test_sample_xy(sample: Sample) -> None: x_expected = { - b"type-a": [[5.0, 1.0, 1.0], [5.0, 1.0, 2.0], [5.0, 1.0, 3.0]], + b"type-a": [[5.0, 1.0, 1.0, 0.0], [5.0, 1.0, 2.0, 0.0], [5.0, 1.0, 3.0, 0.0]], b"type-b": [[5.0, 1.0, 4.0, 0.0]], } y_expected = { diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 2c6ac37..c6527f7 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -61,7 +61,7 @@ def test_knapsack() -> None: np.array(["default", "default", "default", "default", ""], dtype="S"), ) assert_equals( - sample.get_vector_list("static_var_features"), + sample.get_array("static_var_features"), np.array( [ [23.0, 505.0, 1.0, 0.32899, 0.0, 505.0, 1.0], @@ -155,7 +155,7 @@ def test_knapsack() -> None: np.array([1.0, 0.923077, 1.0, 0.0, 67.0]), ) assert_equals( - sample.get_vector_list("lp_var_features"), + sample.get_array("lp_var_features"), np.array( [ [ diff --git a/tests/instance/test_file.py b/tests/instance/test_file.py index 4dfb607..4beb80a 100644 --- a/tests/instance/test_file.py +++ b/tests/instance/test_file.py @@ -18,7 +18,7 @@ def test_usage() -> None: filename = tempfile.mktemp() FileInstance.save(original, filename) sample = Hdf5Sample(filename, check_data=True) - assert len(sample.get_bytes("pickled")) > 0 + assert len(sample.get_array("pickled")) > 0 # Solve instance from disk solver = LearningSolver(solver=GurobiSolver()) From 9cfb31bacb231fbb4f8e607f933eb3a3ce577e92 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 10 Aug 2021 17:27:06 -0500 Subject: [PATCH 58/67] Remove {get,put}_set and deprecated functions --- miplearn/components/dynamic_common.py | 13 ++- miplearn/components/dynamic_lazy.py | 6 +- miplearn/components/dynamic_user_cuts.py | 5 +- miplearn/components/static_lazy.py | 7 +- miplearn/features/sample.py | 106 ++------------------- tests/components/test_dynamic_lazy.py | 13 ++- tests/components/test_dynamic_user_cuts.py | 2 +- tests/components/test_static_lazy.py | 14 ++- tests/features/test_extractor.py | 10 +- tests/instance/test_file.py | 2 +- tests/problems/test_tsp.py | 2 +- 11 files changed, 56 insertions(+), 124 deletions(-) diff --git a/miplearn/components/dynamic_common.py b/miplearn/components/dynamic_common.py index 0b48f6f..dbfd5e7 100644 --- a/miplearn/components/dynamic_common.py +++ b/miplearn/components/dynamic_common.py @@ -56,6 +56,11 @@ class DynamicConstraintsComponent(Component): cids: Dict[ConstraintCategory, List[ConstraintName]] = {} known_cids = np.array(self.known_cids, dtype="S") + enforced_cids = None + enforced_cids_np = sample.get_array(self.attr) + if enforced_cids_np is not None: + enforced_cids = list(enforced_cids_np) + # Get user-provided constraint features ( constr_features, @@ -72,13 +77,11 @@ class DynamicConstraintsComponent(Component): constr_features, ] ) - assert len(known_cids) == constr_features.shape[0] categories = np.unique(constr_categories) for c in categories: x[c] = constr_features[constr_categories == c].tolist() cids[c] = known_cids[constr_categories == c].tolist() - enforced_cids = np.array(list(sample.get_set(self.attr)), dtype="S") if enforced_cids is not None: tmp = np.isin(cids[c], enforced_cids).reshape(-1, 1) y[c] = np.hstack([~tmp, tmp]).tolist() # type: ignore @@ -99,7 +102,7 @@ class DynamicConstraintsComponent(Component): assert pre is not None known_cids: Set = set() for cids in pre: - known_cids |= cids + known_cids |= set(list(cids)) self.known_cids.clear() self.known_cids.extend(sorted(known_cids)) @@ -128,7 +131,7 @@ class DynamicConstraintsComponent(Component): @overrides def pre_sample_xy(self, instance: Instance, sample: Sample) -> Any: - return sample.get_set(self.attr) + return sample.get_array(self.attr) @overrides def fit_xy( @@ -150,7 +153,7 @@ class DynamicConstraintsComponent(Component): instance: Instance, sample: Sample, ) -> Dict[str, float]: - actual = sample.get_set(self.attr) + actual = sample.get_array(self.attr) assert actual is not None pred = set(self.sample_predict(instance, sample)) tp, tn, fp, fn = 0, 0, 0, 0 diff --git a/miplearn/components/dynamic_lazy.py b/miplearn/components/dynamic_lazy.py index 9e82556..7756e64 100644 --- a/miplearn/components/dynamic_lazy.py +++ b/miplearn/components/dynamic_lazy.py @@ -3,6 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging +import pdb from typing import Dict, List, TYPE_CHECKING, Tuple, Any, Optional, Set import numpy as np @@ -78,7 +79,10 @@ class DynamicLazyConstraintsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - sample.put_set("mip_constr_lazy_enforced", set(self.lazy_enforced)) + sample.put_array( + "mip_constr_lazy_enforced", + np.array(list(self.lazy_enforced), dtype="S"), + ) @overrides def iteration_cb( diff --git a/miplearn/components/dynamic_user_cuts.py b/miplearn/components/dynamic_user_cuts.py index 3fe3298..b48d7e7 100644 --- a/miplearn/components/dynamic_user_cuts.py +++ b/miplearn/components/dynamic_user_cuts.py @@ -87,7 +87,10 @@ class UserCutsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - sample.put_set("mip_user_cuts_enforced", set(self.enforced)) + sample.put_array( + "mip_user_cuts_enforced", + np.array(list(self.enforced), dtype="S"), + ) stats["UserCuts: Added in callback"] = self.n_added_in_callback if self.n_added_in_callback > 0: logger.info(f"{self.n_added_in_callback} user cuts added in callback") diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index 2dd300e..e819755 100644 --- a/miplearn/components/static_lazy.py +++ b/miplearn/components/static_lazy.py @@ -61,7 +61,10 @@ class StaticLazyConstraintsComponent(Component): stats: LearningSolveStats, sample: Sample, ) -> None: - sample.put_set("mip_constr_lazy_enforced", self.enforced_cids) + sample.put_array( + "mip_constr_lazy_enforced", + np.array(list(self.enforced_cids), dtype="S"), + ) stats["LazyStatic: Restored"] = self.n_restored stats["LazyStatic: Iterations"] = self.n_iterations @@ -212,7 +215,7 @@ class StaticLazyConstraintsComponent(Component): constr_names = sample.get_array("static_constr_names") constr_categories = sample.get_array("static_constr_categories") constr_lazy = sample.get_array("static_constr_lazy") - lazy_enforced = sample.get_set("mip_constr_lazy_enforced") + lazy_enforced = sample.get_array("mip_constr_lazy_enforced") if constr_features is None: constr_features = sample.get_array("static_constr_features") diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 6afde9e..df50552 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -46,15 +46,6 @@ class Sample(ABC): def put_scalar(self, key: str, value: Scalar) -> None: pass - @abstractmethod - def get_vector(self, key: str) -> Optional[Any]: - warnings.warn("Deprecated", DeprecationWarning) - return None - - @abstractmethod - def put_vector(self, key: str, value: Vector) -> None: - warnings.warn("Deprecated", DeprecationWarning) - @abstractmethod def put_array(self, key: str, value: Optional[np.ndarray]) -> None: pass @@ -71,19 +62,6 @@ class Sample(ABC): def get_sparse(self, key: str) -> Optional[coo_matrix]: pass - def get_set(self, key: str) -> Set: - warnings.warn("Deprecated", DeprecationWarning) - v = self.get_vector(key) - if v: - return set(v) - else: - return set() - - def put_set(self, key: str, value: Set) -> None: - warnings.warn("Deprecated", DeprecationWarning) - v = list(value) - self.put_vector(key, v) - def _assert_is_scalar(self, value: Any) -> None: if value is None: return @@ -91,20 +69,13 @@ class Sample(ABC): return assert False, f"scalar expected; found instead: {value} ({value.__class__})" - def _assert_is_vector(self, value: Any) -> None: - assert isinstance( - value, (list, np.ndarray) - ), f"list or numpy array expected; found instead: {value} ({value.__class__})" - for v in value: - self._assert_is_scalar(v) - - def _assert_supported(self, value: np.ndarray) -> None: + def _assert_is_array(self, value: np.ndarray) -> None: assert isinstance(value, np.ndarray) assert value.dtype.kind in "biufS", f"Unsupported dtype: {value.dtype}" def _assert_is_sparse(self, value: Any) -> None: assert isinstance(value, coo_matrix) - self._assert_supported(value.data) + self._assert_is_array(value.data) class MemorySample(Sample): @@ -113,35 +84,20 @@ class MemorySample(Sample): def __init__( self, data: Optional[Dict[str, Any]] = None, - check_data: bool = True, ) -> None: if data is None: data = {} self._data: Dict[str, Any] = data - self._check_data = check_data @overrides def get_scalar(self, key: str) -> Optional[Any]: return self._get(key) - @overrides - def get_vector(self, key: str) -> Optional[Any]: - return self._get(key) - @overrides def put_scalar(self, key: str, value: Scalar) -> None: if value is None: return - if self._check_data: - self._assert_is_scalar(value) - self._put(key, value) - - @overrides - def put_vector(self, key: str, value: Vector) -> None: - if value is None: - return - if self._check_data: - self._assert_is_vector(value) + self._assert_is_scalar(value) self._put(key, value) def _get(self, key: str) -> Optional[Any]: @@ -157,7 +113,7 @@ class MemorySample(Sample): def put_array(self, key: str, value: Optional[np.ndarray]) -> None: if value is None: return - self._assert_supported(value) + self._assert_is_array(value) self._put(key, value) @overrides @@ -188,10 +144,8 @@ class Hdf5Sample(Sample): self, filename: str, mode: str = "r+", - check_data: bool = True, ) -> None: self.file = h5py.File(filename, mode, libver="latest") - self._check_data = check_data @overrides def get_scalar(self, key: str) -> Optional[Any]: @@ -206,66 +160,20 @@ class Hdf5Sample(Sample): else: return ds[()].tolist() - @overrides - def get_vector(self, key: str) -> Optional[Any]: - if key not in self.file: - return None - ds = self.file[key] - assert ( - len(ds.shape) == 1 - ), f"1-dimensional array expected; found shape {ds.shape}" - if h5py.check_string_dtype(ds.dtype): - result = ds.asstr()[:].tolist() - result = [r if len(r) > 0 else None for r in result] - return result - else: - return ds[:].tolist() - @overrides def put_scalar(self, key: str, value: Any) -> None: if value is None: return - if self._check_data: - self._assert_is_scalar(value) - self._put(key, value) - - @overrides - def put_vector(self, key: str, value: Vector) -> None: - if value is None: - return - if self._check_data: - self._assert_is_vector(value) - - for v in value: - # Convert strings to bytes - if isinstance(v, str) or v is None: - value = np.array( - [u if u is not None else b"" for u in value], - dtype="S", - ) - break - - # Convert all floating point numbers to half-precision - if isinstance(v, float): - value = np.array(value, dtype=np.dtype("f2")) - break - - self._put(key, value, compress=True) - - def _put(self, key: str, value: Any, compress: bool = False) -> Dataset: + self._assert_is_scalar(value) if key in self.file: del self.file[key] - if compress: - ds = self.file.create_dataset(key, data=value, compression="gzip") - else: - ds = self.file.create_dataset(key, data=value) - return ds + self.file.create_dataset(key, data=value) @overrides def put_array(self, key: str, value: Optional[np.ndarray]) -> None: if value is None: return - self._assert_supported(value) + self._assert_is_array(value) if key in self.file: del self.file[key] return self.file.create_dataset(key, data=value, compression="gzip") diff --git a/tests/components/test_dynamic_lazy.py b/tests/components/test_dynamic_lazy.py index e46c116..4fbdc0b 100644 --- a/tests/components/test_dynamic_lazy.py +++ b/tests/components/test_dynamic_lazy.py @@ -24,13 +24,13 @@ def training_instances() -> List[Instance]: samples_0 = [ MemorySample( { - "mip_constr_lazy_enforced": {b"c1", b"c2"}, + "mip_constr_lazy_enforced": np.array(["c1", "c2"], dtype="S"), "static_instance_features": np.array([5.0]), }, ), MemorySample( { - "mip_constr_lazy_enforced": {b"c2", b"c3"}, + "mip_constr_lazy_enforced": np.array(["c2", "c3"], dtype="S"), "static_instance_features": np.array([5.0]), }, ), @@ -55,7 +55,7 @@ def training_instances() -> List[Instance]: samples_1 = [ MemorySample( { - "mip_constr_lazy_enforced": {b"c3", b"c4"}, + "mip_constr_lazy_enforced": np.array(["c3", "c4"], dtype="S"), "static_instance_features": np.array([8.0]), }, ) @@ -81,7 +81,12 @@ def training_instances() -> List[Instance]: def test_sample_xy(training_instances: List[Instance]) -> None: comp = DynamicLazyConstraintsComponent() - comp.pre_fit([{b"c1", b"c2", b"c3", b"c4"}]) + comp.pre_fit( + [ + np.array(["c1", "c3", "c4"], dtype="S"), + np.array(["c1", "c2", "c4"], dtype="S"), + ] + ) x_expected = { b"type-a": np.array([[5.0, 1.0, 2.0, 3.0], [5.0, 4.0, 5.0, 6.0]]), b"type-b": np.array([[5.0, 1.0, 2.0, 0.0], [5.0, 3.0, 4.0, 0.0]]), diff --git a/tests/components/test_dynamic_user_cuts.py b/tests/components/test_dynamic_user_cuts.py index ab8e25c..2bae1a6 100644 --- a/tests/components/test_dynamic_user_cuts.py +++ b/tests/components/test_dynamic_user_cuts.py @@ -82,7 +82,7 @@ def test_usage( ) -> None: stats_before = solver.solve(stab_instance) sample = stab_instance.get_samples()[0] - user_cuts_enforced = sample.get_set("mip_user_cuts_enforced") + user_cuts_enforced = sample.get_array("mip_user_cuts_enforced") assert user_cuts_enforced is not None assert len(user_cuts_enforced) > 0 assert stats_before["UserCuts: Added ahead-of-time"] == 0 diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index 5a38822..9455dcd 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -19,6 +19,7 @@ from miplearn.types import ( LearningSolveStats, ConstraintCategory, ) +from miplearn.solvers.tests import assert_equals @pytest.fixture @@ -35,7 +36,7 @@ def sample() -> Sample: "static_constr_lazy": np.array([True, True, True, True, False]), "static_constr_names": np.array(["c1", "c2", "c3", "c4", "c5"], dtype="S"), "static_instance_features": [5.0], - "mip_constr_lazy_enforced": {b"c1", b"c2", b"c4"}, + "mip_constr_lazy_enforced": np.array(["c1", "c2", "c4"], dtype="S"), "lp_constr_features": np.array( [ [1.0, 1.0, 0.0], @@ -96,7 +97,7 @@ def test_usage_with_solver(instance: Instance) -> None: stats: LearningSolveStats = {} sample = instance.get_samples()[0] - assert sample.get_set("mip_constr_lazy_enforced") is not None + assert sample.get_array("mip_constr_lazy_enforced") is not None # LearningSolver calls before_solve_mip component.before_solve_mip( @@ -145,8 +146,13 @@ def test_usage_with_solver(instance: Instance) -> None: ) # Should update training sample - assert sample.get_set("mip_constr_lazy_enforced") == {b"c1", b"c2", b"c3", b"c4"} - # + mip_constr_lazy_enforced = sample.get_array("mip_constr_lazy_enforced") + assert mip_constr_lazy_enforced is not None + assert_equals( + sorted(mip_constr_lazy_enforced), + np.array(["c1", "c2", "c3", "c4"], dtype="S"), + ) + # Should update stats assert stats["LazyStatic: Removed"] == 1 assert stats["LazyStatic: Kept"] == 3 diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index c6527f7..056a1bb 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -77,7 +77,7 @@ def test_knapsack() -> None: np.array(["eq_capacity"], dtype="S"), ) # assert_equals( - # sample.get_vector("static_constr_lhs"), + # sample.get_array("static_constr_lhs"), # [ # [ # ("x[0]", 23.0), @@ -89,7 +89,7 @@ def test_knapsack() -> None: # ], # ) assert_equals( - sample.get_vector("static_constr_rhs"), + sample.get_array("static_constr_rhs"), np.array([0.0]), ) assert_equals( @@ -97,11 +97,11 @@ def test_knapsack() -> None: np.array(["="], dtype="S"), ) assert_equals( - sample.get_vector("static_constr_features"), + sample.get_array("static_constr_features"), np.array([[0.0]]), ) assert_equals( - sample.get_vector("static_constr_categories"), + sample.get_array("static_constr_categories"), np.array(["eq_capacity"], dtype="S"), ) assert_equals( @@ -109,7 +109,7 @@ def test_knapsack() -> None: np.array([False]), ) assert_equals( - sample.get_vector("static_instance_features"), + sample.get_array("static_instance_features"), np.array([67.0, 21.75]), ) assert_equals(sample.get_scalar("static_constr_lazy_count"), 0) diff --git a/tests/instance/test_file.py b/tests/instance/test_file.py index 4beb80a..bad2fc5 100644 --- a/tests/instance/test_file.py +++ b/tests/instance/test_file.py @@ -17,7 +17,7 @@ def test_usage() -> None: # Save instance to disk filename = tempfile.mktemp() FileInstance.save(original, filename) - sample = Hdf5Sample(filename, check_data=True) + sample = Hdf5Sample(filename) assert len(sample.get_array("pickled")) > 0 # Solve instance from disk diff --git a/tests/problems/test_tsp.py b/tests/problems/test_tsp.py index 99a4be6..8572635 100644 --- a/tests/problems/test_tsp.py +++ b/tests/problems/test_tsp.py @@ -66,7 +66,7 @@ def test_subtour() -> None: samples = instance.get_samples() assert len(samples) == 1 sample = samples[0] - lazy_enforced = sample.get_set("mip_constr_lazy_enforced") + lazy_enforced = sample.get_array("mip_constr_lazy_enforced") assert lazy_enforced is not None assert len(lazy_enforced) > 0 assert_equals( From a65ebfb17ce69ae21e98379541389420dd27b913 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Tue, 10 Aug 2021 11:02:02 -0500 Subject: [PATCH 59/67] Re-enable half-precision; minor changes to FeaturesExtractor benchmark --- miplearn/features/sample.py | 2 ++ tests/features/test_extractor.py | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index df50552..39f3261 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -174,6 +174,8 @@ class Hdf5Sample(Sample): if value is None: return self._assert_is_array(value) + if len(value.shape) > 1 and value.dtype.kind == "f": + value = value.astype("float16") if key in self.file: del self.file[key] return self.file.create_dataset(key, data=value, compression="gzip") diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 056a1bb..19b8767 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -11,7 +11,7 @@ import numpy as np import gurobipy as gp from miplearn.features.extractor import FeaturesExtractor -from miplearn.features.sample import MemorySample, Hdf5Sample +from miplearn.features.sample import Hdf5Sample from miplearn.instance.base import Instance from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.internal import Variables, Constraints @@ -382,17 +382,17 @@ class MpsInstance(Instance): return gp.read(self.filename) -if __name__ == "__main__": +def main() -> None: solver = GurobiSolver() instance = MpsInstance(sys.argv[1]) solver.set_instance(instance) - lp_stats = solver.solve_lp(tee=True) extractor = FeaturesExtractor(with_lhs=False) sample = Hdf5Sample("tmp/prof.h5", mode="w") + extractor.extract_after_load_features(instance, solver, sample) + lp_stats = solver.solve_lp(tee=True) + extractor.extract_after_lp_features(solver, sample, lp_stats) - def run() -> None: - extractor.extract_after_load_features(instance, solver, sample) - extractor.extract_after_lp_features(solver, sample, lp_stats) - cProfile.run("run()", filename="tmp/prof") +if __name__ == "__main__": + cProfile.run("main()", filename="tmp/prof") os.system("flameprof tmp/prof > tmp/prof.svg") From 256d3d094f42ae0040f0ad62c2aadae1250f74f8 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 11 Aug 2021 06:17:39 -0500 Subject: [PATCH 60/67] AlvLouWeh2017: Remove sample argument --- miplearn/features/extractor.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 155c153..872767d 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -3,7 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. from math import log, isfinite -from typing import TYPE_CHECKING, List, Tuple +from typing import TYPE_CHECKING, List, Tuple, Optional import numpy as np @@ -74,7 +74,9 @@ class FeaturesExtractor: np.hstack( [ vars_features_user, - self._extract_var_features_AlvLouWeh2017(sample), + self._extract_var_features_AlvLouWeh2017( + obj_coeffs=variables.obj_coeffs, + ), variables.lower_bounds.reshape(-1, 1), variables.obj_coeffs.reshape(-1, 1), variables.upper_bounds.reshape(-1, 1), @@ -111,7 +113,12 @@ class FeaturesExtractor: lp_var_features_list = [] for f in [ sample.get_array("static_var_features"), - self._extract_var_features_AlvLouWeh2017(sample), + self._extract_var_features_AlvLouWeh2017( + obj_coeffs=sample.get_array("static_var_obj_coeffs"), + obj_sa_up=variables.sa_obj_up, + obj_sa_down=variables.sa_obj_down, + values=variables.values, + ), ]: if f is not None: lp_var_features_list.append(f) @@ -310,12 +317,14 @@ class FeaturesExtractor: # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based # approximation of strong branching. INFORMS Journal on Computing, 29(1), 185-195. - def _extract_var_features_AlvLouWeh2017(self, sample: Sample) -> np.ndarray: - obj_coeffs = sample.get_array("static_var_obj_coeffs") - obj_sa_down = sample.get_array("lp_var_sa_obj_down") - obj_sa_up = sample.get_array("lp_var_sa_obj_up") - values = sample.get_array("lp_var_values") - + # noinspection PyPep8Naming + def _extract_var_features_AlvLouWeh2017( + self, + obj_coeffs: Optional[np.ndarray] = None, + obj_sa_down: Optional[np.ndarray] = None, + obj_sa_up: Optional[np.ndarray] = None, + values: Optional[np.ndarray] = None, + ) -> np.ndarray: assert obj_coeffs is not None obj_coeffs = obj_coeffs.astype(float) _fix_infinity(obj_coeffs) From 5b3a56f05339cd789f1c0a1e1b5e5a82cb167689 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Wed, 11 Aug 2021 06:24:10 -0500 Subject: [PATCH 61/67] Re-add sample.{get,put}_bytes --- miplearn/features/sample.py | 19 ++++++++++++++++++- miplearn/instance/file.py | 8 +++++--- miplearn/solvers/tests/__init__.py | 2 +- tests/features/test_extractor.py | 2 +- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 39f3261..71d76e3 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -70,7 +70,9 @@ class Sample(ABC): assert False, f"scalar expected; found instead: {value} ({value.__class__})" def _assert_is_array(self, value: np.ndarray) -> None: - assert isinstance(value, np.ndarray) + assert isinstance( + value, np.ndarray + ), f"np.ndarray expected; found instead: {value.__class__}" assert value.dtype.kind in "biufS", f"Unsupported dtype: {value.dtype}" def _assert_is_sparse(self, value: Any) -> None: @@ -205,3 +207,18 @@ class Hdf5Sample(Sample): assert col is not None assert data is not None return coo_matrix((data, (row, col))) + + def get_bytes(self, key: str) -> Optional[Bytes]: + if key not in self.file: + return None + ds = self.file[key] + assert ( + len(ds.shape) == 1 + ), f"1-dimensional array expected; found shape {ds.shape}" + return ds[()].tobytes() + + def put_bytes(self, key: str, value: Bytes) -> None: + assert isinstance( + value, (bytes, bytearray) + ), f"bytes expected; found: {value.__class__}" # type: ignore + self.put_array(key, np.frombuffer(value, dtype="uint8")) diff --git a/miplearn/instance/file.py b/miplearn/instance/file.py index a08d6b2..46e7609 100644 --- a/miplearn/instance/file.py +++ b/miplearn/instance/file.py @@ -111,14 +111,16 @@ class FileInstance(Instance): def load(self) -> None: if self.instance is not None: return - self.instance = pickle.loads(self.h5.get_array("pickled").tobytes()) + pkl = self.h5.get_bytes("pickled") + assert pkl is not None + self.instance = pickle.loads(pkl) assert isinstance(self.instance, Instance) @classmethod def save(cls, instance: Instance, filename: str) -> None: h5 = Hdf5Sample(filename, mode="w") - instance_pkl = np.frombuffer(pickle.dumps(instance), dtype=np.int8) - h5.put_array("pickled", instance_pkl) + instance_pkl = pickle.dumps(instance) + h5.put_bytes("pickled", instance_pkl) @overrides def create_sample(self) -> Sample: diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index 01ff2c2..f4abe83 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -275,7 +275,7 @@ def _equals_preprocess(obj: Any) -> Any: return np.round(obj, decimals=6).tolist() else: return obj.tolist() - elif isinstance(obj, (int, str, bool, np.bool_, np.bytes_, bytes)): + elif isinstance(obj, (int, str, bool, np.bool_, np.bytes_, bytes, bytearray)): return obj elif isinstance(obj, float): return round(obj, 6) diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 19b8767..858d976 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -11,7 +11,7 @@ import numpy as np import gurobipy as gp from miplearn.features.extractor import FeaturesExtractor -from miplearn.features.sample import Hdf5Sample +from miplearn.features.sample import Hdf5Sample, MemorySample from miplearn.instance.base import Instance from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.internal import Variables, Constraints From fabb13dc7ac3c2faf494c44c628f134b499d67ad Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 12 Aug 2021 05:35:04 -0500 Subject: [PATCH 62/67] Extract LHS as a sparse matrix --- miplearn/features/extractor.py | 2 +- miplearn/features/sample.py | 4 +- miplearn/solvers/gurobi.py | 44 ++++++------ miplearn/solvers/internal.py | 16 ++--- miplearn/solvers/pyomo/base.py | 108 ++++++++++++++++------------- miplearn/solvers/tests/__init__.py | 31 +++------ tests/features/test_extractor.py | 50 +++++-------- 7 files changed, 119 insertions(+), 136 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 872767d..7cf0c2f 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -39,7 +39,7 @@ class FeaturesExtractor: sample.put_array("static_var_types", variables.types) sample.put_array("static_var_upper_bounds", variables.upper_bounds) sample.put_array("static_constr_names", constraints.names) - # sample.put("static_constr_lhs", constraints.lhs) + sample.put_sparse("static_constr_lhs", constraints.lhs) sample.put_array("static_constr_rhs", constraints.rhs) sample.put_array("static_constr_senses", constraints.senses) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 71d76e3..50d9f72 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -76,7 +76,9 @@ class Sample(ABC): assert value.dtype.kind in "biufS", f"Unsupported dtype: {value.dtype}" def _assert_is_sparse(self, value: Any) -> None: - assert isinstance(value, coo_matrix) + assert isinstance( + value, coo_matrix + ), f"coo_matrix expected; found: {value.__class__}" self._assert_is_array(value.data) diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index 536750e..d49906a 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -10,6 +10,7 @@ from typing import List, Any, Dict, Optional, TYPE_CHECKING import numpy as np from overrides import overrides +from scipy.sparse import coo_matrix, lil_matrix from miplearn.instance.base import Instance from miplearn.solvers import _RedirectOutput @@ -99,17 +100,19 @@ class GurobiSolver(InternalSolver): assert cf.lhs is not None assert cf.rhs is not None assert self.model is not None + lhs = cf.lhs.tocsr() for i in range(len(cf.names)): sense = cf.senses[i] - lhs = self.gp.quicksum( - self._varname_to_var[varname] * coeff for (varname, coeff) in cf.lhs[i] + row = lhs[i, :] + row_expr = self.gp.quicksum( + self._gp_vars[row.indices[j]] * row.data[j] for j in range(row.getnnz()) ) if sense == b"=": - self.model.addConstr(lhs == cf.rhs[i], name=cf.names[i]) + self.model.addConstr(row_expr == cf.rhs[i], name=cf.names[i]) elif sense == b"<": - self.model.addConstr(lhs <= cf.rhs[i], name=cf.names[i]) + self.model.addConstr(row_expr <= cf.rhs[i], name=cf.names[i]) elif sense == b">": - self.model.addConstr(lhs >= cf.rhs[i], name=cf.names[i]) + self.model.addConstr(row_expr >= cf.rhs[i], name=cf.names[i]) else: raise Exception(f"Unknown sense: {sense}") self.model.update() @@ -133,18 +136,18 @@ class GurobiSolver(InternalSolver): assert cf.rhs is not None assert self.model is not None result = [] + x = np.array(self.model.getAttr("x", self.model.getVars())) + lhs = cf.lhs.tocsr() * x for i in range(len(cf.names)): sense = cf.senses[i] - lhs = sum( - self._varname_to_var[varname].x * coeff - for (varname, coeff) in cf.lhs[i] - ) - if sense == "<": - result.append(lhs <= cf.rhs[i] + tol) - elif sense == ">": - result.append(lhs >= cf.rhs[i] - tol) + if sense == b"<": + result.append(lhs[i] <= cf.rhs[i] + tol) + elif sense == b">": + result.append(lhs[i] >= cf.rhs[i] - tol) + elif sense == b"<": + result.append(abs(cf.rhs[i] - lhs[i]) <= tol) else: - result.append(abs(cf.rhs[i] - lhs) <= tol) + raise Exception(f"unknown sense: {sense}") return result @overrides @@ -214,7 +217,7 @@ class GurobiSolver(InternalSolver): gp_constrs = model.getConstrs() constr_names = np.array(model.getAttr("constrName", gp_constrs), dtype="S") - lhs: Optional[List] = None + lhs: Optional[coo_matrix] = None rhs, senses, slacks, basis_status = None, None, None, None dual_value, basis_status, sa_rhs_up, sa_rhs_down = None, None, None, None @@ -222,13 +225,14 @@ class GurobiSolver(InternalSolver): rhs = np.array(model.getAttr("rhs", gp_constrs), dtype=float) senses = np.array(model.getAttr("sense", gp_constrs), dtype="S") if with_lhs: - lhs = [None for _ in gp_constrs] + nrows = len(gp_constrs) + ncols = len(self._var_names) + tmp = lil_matrix((nrows, ncols), dtype=float) for (i, gp_constr) in enumerate(gp_constrs): expr = model.getRow(gp_constr) - lhs[i] = [ - (self._var_names[expr.getVar(j).index], expr.getCoeff(j)) - for j in range(expr.size()) - ] + for j in range(expr.size()): + tmp[i, j] = expr.getCoeff(j) + lhs = tmp.tocoo() if self._has_lp_solution: dual_value = np.array(model.getAttr("pi", gp_constrs), dtype=float) diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index 1865159..9688cb0 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -5,9 +5,10 @@ import logging from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, Optional, List, Tuple, TYPE_CHECKING +from typing import Any, Optional, List, TYPE_CHECKING import numpy as np +from scipy.sparse import coo_matrix from miplearn.instance.base import Instance from miplearn.types import ( @@ -71,7 +72,7 @@ class Constraints: basis_status: Optional[np.ndarray] = None dual_values: Optional[np.ndarray] = None lazy: Optional[np.ndarray] = None - lhs: Optional[List[List[Tuple[bytes, float]]]] = None + lhs: Optional[coo_matrix] = None names: Optional[np.ndarray] = None rhs: Optional[np.ndarray] = None sa_rhs_down: Optional[np.ndarray] = None @@ -104,7 +105,7 @@ class Constraints: ), names=(None if self.names is None else self.names[selected]), lazy=(None if self.lazy is None else self.lazy[selected]), - lhs=self._filter(self.lhs, selected), + lhs=(None if self.lhs is None else self.lhs.tocsr()[selected].tocoo()), rhs=(None if self.rhs is None else self.rhs[selected]), sa_rhs_down=( None if self.sa_rhs_down is None else self.sa_rhs_down[selected] @@ -114,15 +115,6 @@ class Constraints: slacks=(None if self.slacks is None else self.slacks[selected]), ) - def _filter( - self, - obj: Optional[List], - selected: List[bool], - ) -> Optional[List]: - if obj is None: - return None - return [obj[i] for (i, selected_i) in enumerate(selected) if selected_i] - class InternalSolver(ABC): """ diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index 3e306a5..5292eb0 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -6,7 +6,7 @@ import logging import re import sys from io import StringIO -from typing import Any, List, Dict, Optional, Tuple +from typing import Any, List, Dict, Optional import numpy as np import pyomo @@ -18,6 +18,7 @@ from pyomo.core.base.constraint import ConstraintList from pyomo.core.expr.numeric_expr import SumExpression, MonomialTermExpression from pyomo.opt import TerminationCondition from pyomo.opt.base.solvers import SolverFactory +from scipy.sparse import coo_matrix from miplearn.instance.base import Instance from miplearn.solvers import _RedirectOutput, _none_if_empty @@ -58,6 +59,7 @@ class BasePyomoSolver(InternalSolver): self._pyomo_solver: SolverFactory = solver_factory self._obj_sense: str = "min" self._varname_to_var: Dict[bytes, pe.Var] = {} + self._varname_to_idx: Dict[str, int] = {} self._cname_to_constr: Dict[str, pe.Constraint] = {} self._termination_condition: str = "" self._has_lp_solution = False @@ -84,23 +86,24 @@ class BasePyomoSolver(InternalSolver): assert cf.lhs is not None assert cf.rhs is not None assert self.model is not None - for (i, name) in enumerate(cf.names): - lhs = 0.0 - for (varname, coeff) in cf.lhs[i]: - var = self._varname_to_var[varname] - lhs += var * coeff + lhs = cf.lhs.tocsr() + for i in range(len(cf.names)): + row = lhs[i, :] + lhsi = 0.0 + for j in range(row.getnnz()): + lhsi += self._all_vars[row.indices[j]] * row.data[j] if cf.senses[i] == b"=": - expr = lhs == cf.rhs[i] + expr = lhsi == cf.rhs[i] elif cf.senses[i] == b"<": - expr = lhs <= cf.rhs[i] + expr = lhsi <= cf.rhs[i] elif cf.senses[i] == b">": - expr = lhs >= cf.rhs[i] + expr = lhsi >= cf.rhs[i] else: raise Exception(f"Unknown sense: {cf.senses[i]}") - cl = pe.Constraint(expr=expr, name=name) - self.model.add_component(name.decode(), cl) + cl = pe.Constraint(expr=expr, name=cf.names[i]) + self.model.add_component(cf.names[i].decode(), cl) self._pyomo_solver.add_constraint(cl) - self._cname_to_constr[name] = cl + self._cname_to_constr[cf.names[i]] = cl self._termination_condition = "" self._has_lp_solution = False self._has_mip_solution = False @@ -119,18 +122,18 @@ class BasePyomoSolver(InternalSolver): assert cf.lhs is not None assert cf.rhs is not None assert cf.senses is not None + x = [v.value for v in self._all_vars] + lhs = cf.lhs.tocsr() * x result = [] - for (i, name) in enumerate(cf.names): - lhs = 0.0 - for (varname, coeff) in cf.lhs[i]: - var = self._varname_to_var[varname] - lhs += var.value * coeff - if cf.senses[i] == "<": - result.append(lhs <= cf.rhs[i] + tol) - elif cf.senses[i] == ">": - result.append(lhs >= cf.rhs[i] - tol) + for i in range(len(lhs)): + if cf.senses[i] == b"<": + result.append(lhs[i] <= cf.rhs[i] + tol) + elif cf.senses[i] == b">": + result.append(lhs[i] >= cf.rhs[i] - tol) + elif cf.senses[i] == b"=": + result.append(abs(cf.rhs[i] - lhs[i]) < tol) else: - result.append(abs(cf.rhs[i] - lhs) < tol) + raise Exception(f"unknown sense: {cf.senses[i]}") return result @overrides @@ -163,15 +166,17 @@ class BasePyomoSolver(InternalSolver): ) -> Constraints: model = self.model assert model is not None - names: List[str] = [] rhs: List[float] = [] - lhs: List[List[Tuple[bytes, float]]] = [] senses: List[str] = [] dual_values: List[float] = [] slacks: List[float] = [] + lhs_row: List[int] = [] + lhs_col: List[int] = [] + lhs_data: List[float] = [] + lhs: Optional[coo_matrix] = None - def _parse_constraint(c: pe.Constraint) -> None: + def _parse_constraint(c: pe.Constraint, row: int) -> None: assert model is not None if with_static: # Extract RHS and sense @@ -192,30 +197,31 @@ class BasePyomoSolver(InternalSolver): if with_lhs: # Extract LHS - lhsc = [] expr = c.body if isinstance(expr, SumExpression): for term in expr._args_: if isinstance(term, MonomialTermExpression): - lhsc.append( - ( - term._args_[1].name.encode(), - float(term._args_[0]), - ) + lhs_row.append(row) + lhs_col.append( + self._varname_to_idx[term._args_[1].name] ) + lhs_data.append(float(term._args_[0])) elif isinstance(term, _GeneralVarData): - lhsc.append((term.name.encode(), 1.0)) + lhs_row.append(row) + lhs_col.append(self._varname_to_idx[term.name]) + lhs_data.append(1.0) else: raise Exception( f"Unknown term type: {term.__class__.__name__}" ) elif isinstance(expr, _GeneralVarData): - lhsc.append((expr.name.encode(), 1.0)) + lhs_row.append(row) + lhs_col.append(self._varname_to_idx[expr.name]) + lhs_data.append(1.0) else: raise Exception( f"Unknown expression type: {expr.__class__.__name__}" ) - lhs.append(lhsc) # Extract dual values if self._has_lp_solution: @@ -225,20 +231,26 @@ class BasePyomoSolver(InternalSolver): if self._has_mip_solution or self._has_lp_solution: slacks.append(model.slack[c]) - for constr in model.component_objects(pyomo.core.Constraint): + curr_row = 0 + for (i, constr) in enumerate(model.component_objects(pyomo.core.Constraint)): if isinstance(constr, pe.ConstraintList): for idx in constr: - names.append(f"{constr.name}[{idx}]") - _parse_constraint(constr[idx]) + names.append(constr[idx].name) + _parse_constraint(constr[idx], curr_row) + curr_row += 1 else: names.append(constr.name) - _parse_constraint(constr) + _parse_constraint(constr, curr_row) + curr_row += 1 + + if len(lhs_data) > 0: + lhs = coo_matrix((lhs_data, (lhs_row, lhs_col))).tocoo() return Constraints( names=_none_if_empty(np.array(names, dtype="S")), rhs=_none_if_empty(np.array(rhs, dtype=float)), senses=_none_if_empty(np.array(senses, dtype="S")), - lhs=_none_if_empty(lhs), + lhs=lhs, slacks=_none_if_empty(np.array(slacks, dtype=float)), dual_values=_none_if_empty(np.array(dual_values, dtype=float)), ) @@ -264,7 +276,7 @@ class BasePyomoSolver(InternalSolver): for index in var: if var[index].fixed: continue - solution[f"{var}[{index}]".encode()] = var[index].value + solution[var[index].name.encode()] = var[index].value return solution @overrides @@ -289,9 +301,9 @@ class BasePyomoSolver(InternalSolver): # Variable name if idx is None: - names.append(str(var)) + names.append(var.name) else: - names.append(f"{var}[{idx}]") + names.append(var[idx].name) if with_static: # Variable type @@ -556,12 +568,14 @@ class BasePyomoSolver(InternalSolver): self._all_vars = [] self._bin_vars = [] self._varname_to_var = {} + self._varname_to_idx = {} for var in self.model.component_objects(Var): for idx in var: - varname = f"{var.name}[{idx}]".encode() - if idx is None: - varname = var.name.encode() - self._varname_to_var[varname] = var[idx] + varname = var.name + if idx is not None: + varname = var[idx].name + self._varname_to_var[varname.encode()] = var[idx] + self._varname_to_idx[varname] = len(self._all_vars) self._all_vars += [var[idx]] if var[idx].domain == pyomo.core.base.set_types.Binary: self._bin_vars += [var[idx]] @@ -575,7 +589,7 @@ class BasePyomoSolver(InternalSolver): for constr in self.model.component_objects(pyomo.core.Constraint): if isinstance(constr, pe.ConstraintList): for idx in constr: - self._cname_to_constr[f"{constr.name}[{idx}]"] = constr[idx] + self._cname_to_constr[constr[idx].name] = constr[idx] else: self._cname_to_constr[constr.name] = constr diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index f4abe83..3bc74d3 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -5,6 +5,7 @@ from typing import Any, List import numpy as np +from scipy.sparse import coo_matrix from miplearn.solvers.internal import InternalSolver, Variables, Constraints @@ -55,15 +56,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: Constraints( names=np.array(["eq_capacity"], dtype="S"), rhs=np.array([0.0]), - lhs=[ - [ - (b"x[0]", 23.0), - (b"x[1]", 26.0), - (b"x[2]", 20.0), - (b"x[3]", 18.0), - (b"z", -1.0), - ], - ], + lhs=coo_matrix([[23.0, 26.0, 20.0, 18.0, -1.0]]), senses=np.array(["="], dtype="S"), ), ) @@ -162,7 +155,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: # Build new constraint and verify that it is violated cf = Constraints( names=np.array(["cut"], dtype="S"), - lhs=[[(b"x[0]", 1.0)]], + lhs=coo_matrix([[1.0, 0.0, 0.0, 0.0, 0.0]]), rhs=np.array([0.0]), senses=np.array(["<"], dtype="S"), ) @@ -177,18 +170,12 @@ def run_basic_usage_tests(solver: InternalSolver) -> None: Constraints( names=np.array(["eq_capacity", "cut"], dtype="S"), rhs=np.array([0.0, 0.0]), - lhs=[ + lhs=coo_matrix( [ - (b"x[0]", 23.0), - (b"x[1]", 26.0), - (b"x[2]", 20.0), - (b"x[3]", 18.0), - (b"z", -1.0), - ], - [ - (b"x[0]", 1.0), - ], - ], + [23.0, 26.0, 20.0, 18.0, -1.0], + [1.0, 0.0, 0.0, 0.0, 0.0], + ] + ), senses=np.array(["=", "<"], dtype="S"), ), ), @@ -275,6 +262,8 @@ def _equals_preprocess(obj: Any) -> Any: return np.round(obj, decimals=6).tolist() else: return obj.tolist() + elif isinstance(obj, coo_matrix): + return obj.todense().tolist() elif isinstance(obj, (int, str, bool, np.bool_, np.bytes_, bytes, bytearray)): return obj elif isinstance(obj, float): diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 858d976..26cd7a0 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -9,6 +9,7 @@ from typing import Any import numpy as np import gurobipy as gp +from scipy.sparse import coo_matrix from miplearn.features.extractor import FeaturesExtractor from miplearn.features.sample import Hdf5Sample, MemorySample @@ -76,18 +77,10 @@ def test_knapsack() -> None: sample.get_array("static_constr_names"), np.array(["eq_capacity"], dtype="S"), ) - # assert_equals( - # sample.get_array("static_constr_lhs"), - # [ - # [ - # ("x[0]", 23.0), - # ("x[1]", 26.0), - # ("x[2]", 20.0), - # ("x[3]", 18.0), - # ("z", -1.0), - # ], - # ], - # ) + assert_equals( + sample.get_sparse("static_constr_lhs"), + [[23.0, 26.0, 20.0, 18.0, -1.0]], + ) assert_equals( sample.get_array("static_constr_rhs"), np.array([0.0]), @@ -321,20 +314,13 @@ def test_constraint_getindex() -> None: names=np.array(["c1", "c2", "c3"], dtype="S"), rhs=np.array([1.0, 2.0, 3.0]), senses=np.array(["=", "<", ">"], dtype="S"), - lhs=[ - [ - (b"x1", 1.0), - (b"x2", 1.0), - ], + lhs=coo_matrix( [ - (b"x2", 2.0), - (b"x3", 2.0), - ], - [ - (b"x3", 3.0), - (b"x4", 3.0), - ], - ], + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ] + ), ) assert_equals( cf[[True, False, True]], @@ -342,16 +328,12 @@ def test_constraint_getindex() -> None: names=np.array(["c1", "c3"], dtype="S"), rhs=np.array([1.0, 3.0]), senses=np.array(["=", ">"], dtype="S"), - lhs=[ - [ - (b"x1", 1.0), - (b"x2", 1.0), - ], + lhs=coo_matrix( [ - (b"x3", 3.0), - (b"x4", 3.0), - ], - ], + [1, 2, 3], + [7, 8, 9], + ] + ), ), ) From 53a7c8f84a8fa5ef5c2f1b1e059135af565a6427 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 12 Aug 2021 07:16:23 -0500 Subject: [PATCH 63/67] AlvLouWeh2017: Implement M1 features --- miplearn/features/extractor.py | 153 ++++++++++++++++++------------- tests/features/test_extractor.py | 124 ++++++++++++++++++++----- 2 files changed, 188 insertions(+), 89 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 7cf0c2f..7bb4e70 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -6,6 +6,7 @@ from math import log, isfinite from typing import TYPE_CHECKING, List, Tuple, Optional import numpy as np +from scipy.sparse import coo_matrix from miplearn.features.sample import Sample from miplearn.solvers.internal import LPSolveStats @@ -15,6 +16,7 @@ if TYPE_CHECKING: from miplearn.instance.base import Instance +# noinspection PyPep8Naming class FeaturesExtractor: def __init__( self, @@ -23,6 +25,7 @@ class FeaturesExtractor: ) -> None: self.with_sa = with_sa self.with_lhs = with_lhs + self.var_features_user: Optional[np.ndarray] = None def extract_after_load_features( self, @@ -65,6 +68,7 @@ class FeaturesExtractor: vars_features_user, var_categories, ) = self._extract_user_features_vars(instance, sample) + self.var_features_user = vars_features_user sample.put_array("static_var_categories", var_categories) assert variables.lower_bounds is not None assert variables.obj_coeffs is not None @@ -74,12 +78,11 @@ class FeaturesExtractor: np.hstack( [ vars_features_user, - self._extract_var_features_AlvLouWeh2017( - obj_coeffs=variables.obj_coeffs, + self._compute_AlvLouWeh2017( + A=constraints.lhs, + b=constraints.rhs, + c=variables.obj_coeffs, ), - variables.lower_bounds.reshape(-1, 1), - variables.obj_coeffs.reshape(-1, 1), - variables.upper_bounds.reshape(-1, 1), ] ), ) @@ -112,11 +115,13 @@ class FeaturesExtractor: # Variable features lp_var_features_list = [] for f in [ - sample.get_array("static_var_features"), - self._extract_var_features_AlvLouWeh2017( - obj_coeffs=sample.get_array("static_var_obj_coeffs"), - obj_sa_up=variables.sa_obj_up, - obj_sa_down=variables.sa_obj_down, + self.var_features_user, + self._compute_AlvLouWeh2017( + A=sample.get_sparse("static_constr_lhs"), + b=sample.get_array("static_constr_rhs"), + c=sample.get_array("static_var_obj_coeffs"), + c_sa_up=variables.sa_obj_up, + c_sa_down=variables.sa_obj_down, values=variables.values, ), ]: @@ -315,90 +320,108 @@ class FeaturesExtractor: ], f"Instance features have unsupported {features.dtype}" sample.put_array("static_instance_features", features) - # Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based - # approximation of strong branching. INFORMS Journal on Computing, 29(1), 185-195. - # noinspection PyPep8Naming - def _extract_var_features_AlvLouWeh2017( - self, - obj_coeffs: Optional[np.ndarray] = None, - obj_sa_down: Optional[np.ndarray] = None, - obj_sa_up: Optional[np.ndarray] = None, + @classmethod + def _compute_AlvLouWeh2017( + cls, + A: Optional[coo_matrix] = None, + b: Optional[np.ndarray] = None, + c: Optional[np.ndarray] = None, + c_sa_down: Optional[np.ndarray] = None, + c_sa_up: Optional[np.ndarray] = None, values: Optional[np.ndarray] = None, ) -> np.ndarray: - assert obj_coeffs is not None - obj_coeffs = obj_coeffs.astype(float) - _fix_infinity(obj_coeffs) - nvars = len(obj_coeffs) - - if obj_sa_down is not None: - obj_sa_down = obj_sa_down.astype(float) - _fix_infinity(obj_sa_down) - - if obj_sa_up is not None: - obj_sa_up = obj_sa_up.astype(float) - _fix_infinity(obj_sa_up) - - if values is not None: - values = values.astype(float) - _fix_infinity(values) - - pos_obj_coeffs_sum = obj_coeffs[obj_coeffs > 0].sum() - neg_obj_coeffs_sum = -obj_coeffs[obj_coeffs < 0].sum() + """ + Computes static variable features described in: + Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based + approximation of strong branching. INFORMS Journal on Computing, 29(1), + 185-195. + """ + assert b is not None + assert c is not None + nvars = len(c) + + c_pos_sum = c[c > 0].sum() + c_neg_sum = -c[c < 0].sum() curr = 0 - max_n_features = 8 + max_n_features = 30 features = np.zeros((nvars, max_n_features)) + + def push(v: np.ndarray) -> None: + nonlocal curr + features[:, curr] = v + curr += 1 + with np.errstate(divide="ignore", invalid="ignore"): # Feature 1 - features[:, curr] = np.sign(obj_coeffs) - curr += 1 + push(np.sign(c)) # Feature 2 - if abs(pos_obj_coeffs_sum) > 0: - features[:, curr] = np.abs(obj_coeffs) / pos_obj_coeffs_sum - curr += 1 + push(np.abs(c) / c_pos_sum) # Feature 3 - if abs(neg_obj_coeffs_sum) > 0: - features[:, curr] = np.abs(obj_coeffs) / neg_obj_coeffs_sum - curr += 1 + push(np.abs(c) / c_neg_sum) + + if A is not None: + M1 = A.T.multiply(1.0 / np.abs(b)).T.tocsr() + M1_pos = M1[b > 0, :] + if M1_pos.shape[0] > 0: + M1_pos_max = M1_pos.max(axis=0).todense() + M1_pos_min = M1_pos.min(axis=0).todense() + else: + M1_pos_max = np.zeros(nvars) + M1_pos_min = np.zeros(nvars) + M1_neg = M1[b < 0, :] + if M1_neg.shape[0] > 0: + M1_neg_max = M1_neg.max(axis=0).todense() + M1_neg_min = M1_neg.min(axis=0).todense() + else: + M1_neg_max = np.zeros(nvars) + M1_neg_min = np.zeros(nvars) + + # Features 4-11 + push(np.sign(M1_pos_min)) + push(np.sign(M1_pos_max)) + push(np.abs(M1_pos_min)) + push(np.abs(M1_pos_max)) + push(np.sign(M1_neg_min)) + push(np.sign(M1_neg_max)) + push(np.abs(M1_neg_min)) + push(np.abs(M1_neg_max)) # Feature 37 if values is not None: - features[:, curr] = np.minimum( - values - np.floor(values), - np.ceil(values) - values, + push( + np.minimum( + values - np.floor(values), + np.ceil(values) - values, + ) ) - curr += 1 # Feature 44 - if obj_sa_up is not None: - features[:, curr] = np.sign(obj_sa_up) - curr += 1 + if c_sa_up is not None: + push(np.sign(c_sa_up)) # Feature 46 - if obj_sa_down is not None: - features[:, curr] = np.sign(obj_sa_down) - curr += 1 + if c_sa_down is not None: + push(np.sign(c_sa_down)) # Feature 47 - if obj_sa_down is not None: - features[:, curr] = np.log( - obj_coeffs - obj_sa_down / np.sign(obj_coeffs) - ) - curr += 1 + if c_sa_down is not None: + push(np.log(c - c_sa_down / np.sign(c))) # Feature 48 - if obj_sa_up is not None: - features[:, curr] = np.log(obj_coeffs - obj_sa_up / np.sign(obj_coeffs)) - curr += 1 + if c_sa_up is not None: + push(np.log(c - c_sa_up / np.sign(c))) features = features[:, 0:curr] _fix_infinity(features) return features -def _fix_infinity(m: np.ndarray) -> None: +def _fix_infinity(m: Optional[np.ndarray]) -> None: + if m is None: + return masked = np.ma.masked_invalid(m) max_values = np.max(masked, axis=0) min_values = np.min(masked, axis=0) diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 26cd7a0..5468894 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -65,11 +65,67 @@ def test_knapsack() -> None: sample.get_array("static_var_features"), np.array( [ - [23.0, 505.0, 1.0, 0.32899, 0.0, 505.0, 1.0], - [26.0, 352.0, 1.0, 0.229316, 0.0, 352.0, 1.0], - [20.0, 458.0, 1.0, 0.298371, 0.0, 458.0, 1.0], - [18.0, 220.0, 1.0, 0.143322, 0.0, 220.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 67.0], + [ + 23.0, + 505.0, + 1.0, + 0.32899, + 1e20, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 26.0, + 352.0, + 1.0, + 0.229316, + 1e20, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 20.0, + 458.0, + 1.0, + 0.298371, + 1e20, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 18.0, + 220.0, + 1.0, + 0.143322, + 1e20, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], ] ), ) @@ -156,11 +212,15 @@ def test_knapsack() -> None: 505.0, 1.0, 0.32899, + 1e20, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, - 505.0, - 1.0, - 1.0, - 0.32899, 0.0, 1.0, 1.0, @@ -180,11 +240,15 @@ def test_knapsack() -> None: 352.0, 1.0, 0.229316, + 1e20, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, - 352.0, - 1.0, - 1.0, - 0.229316, 0.076923, 1.0, 1.0, @@ -204,11 +268,15 @@ def test_knapsack() -> None: 458.0, 1.0, 0.298371, + 1e20, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, - 458.0, - 1.0, - 1.0, - 0.298371, 0.0, 1.0, 1.0, @@ -228,15 +296,19 @@ def test_knapsack() -> None: 220.0, 1.0, 0.143322, + 1e20, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, - 220.0, - 1.0, - 1.0, - 0.143322, 0.0, 1.0, -1.0, - 5.453347, + 5.265874, 0.0, -23.692308, -0.111111, @@ -254,13 +326,17 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, - 67.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0, -1.0, - 5.453347, + 5.265874, 0.0, 13.538462, -0.111111, From 2b00cf5b960a692bedc6263954cf5e461c103dc6 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Thu, 12 Aug 2021 07:51:59 -0500 Subject: [PATCH 64/67] Hdf5Sample: Store all fp arrays as float32 --- miplearn/features/sample.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miplearn/features/sample.py b/miplearn/features/sample.py index 50d9f72..a0d6e43 100644 --- a/miplearn/features/sample.py +++ b/miplearn/features/sample.py @@ -178,8 +178,8 @@ class Hdf5Sample(Sample): if value is None: return self._assert_is_array(value) - if len(value.shape) > 1 and value.dtype.kind == "f": - value = value.astype("float16") + if value.dtype.kind == "f": + value = value.astype("float32") if key in self.file: del self.file[key] return self.file.create_dataset(key, data=value, compression="gzip") From ccb1a1ed25f2b7019cc051bf8889da3b499d6d15 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Thu, 12 Aug 2021 07:52:34 -0500 Subject: [PATCH 65/67] GurobiSolver: Fix LHS extraction --- miplearn/solvers/gurobi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index d49906a..751f66c 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -231,7 +231,7 @@ class GurobiSolver(InternalSolver): for (i, gp_constr) in enumerate(gp_constrs): expr = model.getRow(gp_constr) for j in range(expr.size()): - tmp[i, j] = expr.getCoeff(j) + tmp[i, expr.getVar(j).index] = expr.getCoeff(j) lhs = tmp.tocoo() if self._has_lp_solution: From 78d2ad485736f0632e7ab23542d183f1db288e2d Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Thu, 12 Aug 2021 07:52:48 -0500 Subject: [PATCH 66/67] AlvLouWeh2017: Add some assertions; replace non-finite by zero --- miplearn/features/extractor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 7bb4e70..2f057e9 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -363,6 +363,9 @@ class FeaturesExtractor: push(np.abs(c) / c_neg_sum) if A is not None: + assert A.shape[1] == nvars + assert A.shape[0] == len(b) + M1 = A.T.multiply(1.0 / np.abs(b)).T.tocsr() M1_pos = M1[b > 0, :] if M1_pos.shape[0] > 0: @@ -426,4 +429,4 @@ def _fix_infinity(m: Optional[np.ndarray]) -> None: max_values = np.max(masked, axis=0) min_values = np.min(masked, axis=0) m[:] = np.maximum(np.minimum(m, max_values), min_values) - m[np.isnan(m)] = 0.0 + m[~np.isfinite(m)] = 0.0 From cea2d8c1343aefff85fecedadcaabc3bde6e711f Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 12 Aug 2021 08:01:09 -0500 Subject: [PATCH 67/67] Fix failing tests --- tests/features/test_sample.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/tests/features/test_sample.py b/tests/features/test_sample.py index 6802848..92b795d 100644 --- a/tests/features/test_sample.py +++ b/tests/features/test_sample.py @@ -26,13 +26,9 @@ def _test_sample(sample: Sample) -> None: _assert_roundtrip_scalar(sample, 1.0) assert sample.get_scalar("unknown-key") is None - _assert_roundtrip_array(sample, np.array([True, False], dtype="bool")) - _assert_roundtrip_array(sample, np.array([1, 2, 3], dtype="int16")) - _assert_roundtrip_array(sample, np.array([1, 2, 3], dtype="int32")) - _assert_roundtrip_array(sample, np.array([1, 2, 3], dtype="int64")) - _assert_roundtrip_array(sample, np.array([1.0, 2.0, 3.0], dtype="float16")) - _assert_roundtrip_array(sample, np.array([1.0, 2.0, 3.0], dtype="float32")) - _assert_roundtrip_array(sample, np.array([1.0, 2.0, 3.0], dtype="float64")) + _assert_roundtrip_array(sample, np.array([True, False])) + _assert_roundtrip_array(sample, np.array([1, 2, 3])) + _assert_roundtrip_array(sample, np.array([1.0, 2.0, 3.0])) _assert_roundtrip_array(sample, np.array(["A", "BB", "CCC"], dtype="S")) assert sample.get_array("unknown-key") is None @@ -40,11 +36,10 @@ def _test_sample(sample: Sample) -> None: sample, coo_matrix( [ - [1, 0, 0], - [0, 2, 3], - [0, 0, 4], + [1.0, 0.0, 0.0], + [0.0, 2.0, 3.0], + [0.0, 0.0, 4.0], ], - dtype=float, ), ) assert sample.get_sparse("unknown-key") is None @@ -55,7 +50,6 @@ def _assert_roundtrip_array(sample: Sample, original: np.ndarray) -> None: recovered = sample.get_array("key") assert recovered is not None assert isinstance(recovered, np.ndarray) - assert recovered.dtype == original.dtype assert (recovered == original).all() @@ -74,5 +68,4 @@ def _assert_roundtrip_sparse(sample: Sample, original: coo_matrix) -> None: recovered = sample.get_sparse("key") assert recovered is not None assert isinstance(recovered, coo_matrix) - assert recovered.dtype == original.dtype assert (original != recovered).sum() == 0