From 0c4b0ea81af12feb1d9702ce30f81f5e67933bb4 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 5 Aug 2021 15:42:19 -0500 Subject: [PATCH] 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")