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(