From ef9c48d79a65f46c26ccf8f94efb38e11774fdde Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 15 Jul 2021 16:21:40 -0500 Subject: [PATCH] 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]]),