diff --git a/miplearn/components/static_lazy.py b/miplearn/components/static_lazy.py index a252b31..26c3bf6 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 Constraint, Sample, ConstraintFeatures +from miplearn.features import Sample, ConstraintFeatures from miplearn.instance.base import Instance from miplearn.types import LearningSolveStats @@ -45,7 +45,6 @@ class StaticLazyConstraintsComponent(Component): self.threshold_prototype: Threshold = threshold self.classifiers: Dict[Hashable, Classifier] = {} self.thresholds: Dict[Hashable, Threshold] = {} - self.pool_old: Dict[str, Constraint] = {} self.pool: ConstraintFeatures = ConstraintFeatures() self.violation_tolerance: float = violation_tolerance self.enforced_cids: Set[Hashable] = set() diff --git a/miplearn/features.py b/miplearn/features.py index b49b36e..d07c4e6 100644 --- a/miplearn/features.py +++ b/miplearn/features.py @@ -133,42 +133,6 @@ class ConstraintFeatures: return tuple(obj[i] for (i, selected_i) in enumerate(selected) if selected_i) -@dataclass -class Constraint: - basis_status: Optional[str] = None - category: Optional[Hashable] = None - dual_value: Optional[float] = None - lazy: bool = False - lhs: Optional[Dict[str, float]] = None - rhs: float = 0.0 - sa_rhs_down: Optional[float] = None - sa_rhs_up: Optional[float] = None - sense: str = "<" - slack: Optional[float] = None - user_features: Optional[List[float]] = None - - def to_list(self) -> List[float]: - features: List[float] = [] - for attr in [ - "dual value", - "rhs", - "sa_rhs_down", - "sa_rhs_up", - "slack", - ]: - if getattr(self, attr) is not None: - features.append(getattr(self, attr)) - for attr in ["user_features"]: - if getattr(self, attr) is not None: - features.extend(getattr(self, attr)) - if self.lhs is not None and len(self.lhs) > 0: - features.append(np.max(self.lhs.values())) - features.append(np.average(self.lhs.values())) - features.append(np.min(self.lhs.values())) - _clip(features) - return features - - @dataclass class Features: instance: Optional[InstanceFeatures] = None diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index c221723..e296e96 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -10,7 +10,7 @@ from typing import List, Any, Dict, Optional, Hashable, Tuple, TYPE_CHECKING from overrides import overrides -from miplearn.features import Constraint, VariableFeatures, ConstraintFeatures +from miplearn.features import VariableFeatures, ConstraintFeatures from miplearn.instance.base import Instance from miplearn.solvers import _RedirectOutput from miplearn.solvers.internal import ( @@ -81,7 +81,6 @@ class GurobiSolver(InternalSolver): self._var_lbs: Tuple[float, ...] = tuple() self._var_ubs: Tuple[float, ...] = tuple() self._var_obj_coeffs: Tuple[float, ...] = tuple() - self._relaxed_constrs: Dict[str, Tuple["gurobipy.LinExpr", str, float]] = {} if self.lazy_cb_frequency == 1: self.lazy_cb_where = [self.gp.GRB.Callback.MIPSOL] @@ -156,10 +155,6 @@ class GurobiSolver(InternalSolver): capacity=67.0, ) - @overrides - def build_test_instance_redundancy(self) -> Instance: - return GurobiTestInstanceRedundancy() - @overrides def clone(self) -> "GurobiSolver": return GurobiSolver( @@ -167,17 +162,6 @@ class GurobiSolver(InternalSolver): lazy_cb_frequency=self.lazy_cb_frequency, ) - def enforce_constraints(self, names: List[str]) -> None: - assert self.model is not None - constr = [self._relaxed_constrs[n] for n in names] - for (i, (lhs, sense, rhs)) in enumerate(constr): - if sense == "=": - self.model.addConstr(lhs == rhs, name=names[i]) - elif sense == "<": - self.model.addConstr(lhs <= rhs, name=names[i]) - else: - self.model.addConstr(lhs >= rhs, name=names[i]) - @overrides def fix(self, solution: Solution) -> None: self._raise_if_callback() @@ -385,15 +369,6 @@ class GurobiSolver(InternalSolver): assert self.model is not None return self.model.status in [self.gp.GRB.INFEASIBLE, self.gp.GRB.INF_OR_UNBD] - def relax_constraints(self, names: List[str]) -> None: - assert self.model is not None - constrs = [self._cname_to_constr[n] for n in names] - for (i, name) in enumerate(names): - c = constrs[i] - self._relaxed_constrs[name] = self.model.getRow(c), c.sense, c.rhs - self.model.remove(constrs) - self.model.update() - @overrides def remove_constraints(self, names: Tuple[str, ...]) -> None: assert self.model is not None @@ -536,13 +511,6 @@ class GurobiSolver(InternalSolver): lp_wallclock_time=self.model.runtime, ) - @overrides - def relax(self) -> None: - assert self.model is not None - self.model.update() - self.model = self.model.relax() - self._update() - def _apply_params(self, streams: List[Any]) -> None: assert self.model is not None with _RedirectOutput(streams): @@ -666,20 +634,6 @@ class GurobiTestInstanceInfeasible(Instance): return model -class GurobiTestInstanceRedundancy(Instance): - @overrides - def to_model(self) -> Any: - import gurobipy as gp - from gurobipy import GRB - - model = gp.Model() - x = model.addVars(2, vtype=GRB.BINARY, name="x") - model.addConstr(x[0] + x[1] <= 1, name="c1") - model.addConstr(x[0] + x[1] <= 2, name="c2") - model.setObjective(x[0] + x[1], GRB.MAXIMIZE) - return model - - class GurobiTestInstanceKnapsack(PyomoTestInstanceKnapsack): """ Simpler (one-dimensional) knapsack instance, implemented directly in Gurobi diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index 4985b0e..da183b3 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -5,14 +5,13 @@ import logging from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, List, Optional, Tuple -from miplearn.features import Constraint, VariableFeatures, ConstraintFeatures +from miplearn.features import VariableFeatures, ConstraintFeatures from miplearn.instance.base import Instance from miplearn.types import ( IterationCallback, LazyCallback, - BranchPriorities, UserCutCallback, Solution, ) @@ -75,6 +74,9 @@ class InternalSolver(ABC): @abstractmethod def build_test_instance_infeasible(self) -> Instance: + """ + Returns an infeasible instance, for testing purposes. + """ pass @abstractmethod @@ -89,10 +91,6 @@ class InternalSolver(ABC): """ pass - @abstractmethod - def build_test_instance_redundancy(self) -> Instance: - pass - @abstractmethod def clone(self) -> "InternalSolver": """ @@ -184,25 +182,6 @@ class InternalSolver(ABC): """ pass - @abstractmethod - def relax(self) -> None: - """ - Drops all integrality constraints from the model. - """ - pass - - def set_branching_priorities(self, priorities: BranchPriorities) -> None: - """ - Sets the branching priorities for the given decision variables. - - When the MIP solver needs to decide on which variable to branch, variables - with higher priority are picked first, given that they are fractional. - Ties are solved arbitrarily. By default, all variables have priority zero. - - Missing values indicate variables whose priorities should not be modified. - """ - raise NotImplementedError() - @abstractmethod def set_instance( self, diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index bb8ffbb..3d46272 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -136,10 +136,6 @@ class BasePyomoSolver(InternalSolver): def build_test_instance_infeasible(self) -> Instance: return PyomoTestInstanceInfeasible() - @overrides - def build_test_instance_redundancy(self) -> Instance: - return PyomoTestInstanceRedundancy() - @overrides def build_test_instance_knapsack(self) -> Instance: return PyomoTestInstanceKnapsack( @@ -490,7 +486,7 @@ class BasePyomoSolver(InternalSolver): self, tee: bool = False, ) -> LPSolveStats: - self.relax() + self._relax() streams: List[Any] = [StringIO()] if tee: streams += [sys.stdout] @@ -510,15 +506,6 @@ class BasePyomoSolver(InternalSolver): lp_wallclock_time=results["Solver"][0]["Wallclock time"], ) - @overrides - def relax(self) -> None: - for var in self._bin_vars: - lb, ub = var.bounds - var.setlb(lb) - var.setub(ub) - var.domain = pyomo.core.base.set_types.Reals - self._pyomo_solver.update_var(var) - def _clear_warm_start(self) -> None: for var in self._all_vars: if not var.fixed: @@ -575,6 +562,14 @@ class BasePyomoSolver(InternalSolver): raise Exception(f"Unknown expression type: {expr.__class__.__name__}") return lhs + def _relax(self) -> None: + for var in self._bin_vars: + lb, ub = var.bounds + var.setlb(lb) + var.setub(ub) + var.domain = pyomo.core.base.set_types.Reals + self._pyomo_solver.update_var(var) + def _restore_integrality(self) -> None: for var in self._bin_vars: var.domain = pyomo.core.base.set_types.Binary @@ -624,17 +619,6 @@ class PyomoTestInstanceInfeasible(Instance): return model -class PyomoTestInstanceRedundancy(Instance): - @overrides - def to_model(self) -> pe.ConcreteModel: - model = pe.ConcreteModel() - model.x = pe.Var([0, 1], domain=pe.Binary) - model.OBJ = pe.Objective(expr=model.x[0] + model.x[1], sense=pe.maximize) - model.eq1 = pe.Constraint(expr=model.x[0] + model.x[1] <= 1) - model.eq2 = pe.Constraint(expr=model.x[0] + model.x[1] <= 2) - return model - - class PyomoTestInstanceKnapsack(Instance): """ Simpler (one-dimensional) Knapsack Problem, used for testing. diff --git a/miplearn/solvers/pyomo/gurobi.py b/miplearn/solvers/pyomo/gurobi.py index bb1a1a8..162c668 100644 --- a/miplearn/solvers/pyomo/gurobi.py +++ b/miplearn/solvers/pyomo/gurobi.py @@ -10,7 +10,7 @@ from pyomo import environ as pe from scipy.stats import randint from miplearn.solvers.pyomo.base import BasePyomoSolver -from miplearn.types import SolverParams, BranchPriorities +from miplearn.types import SolverParams logger = logging.getLogger(__name__) @@ -42,17 +42,6 @@ class GurobiPyomoSolver(BasePyomoSolver): def clone(self) -> "GurobiPyomoSolver": return GurobiPyomoSolver(params=self.params) - @overrides - def set_branching_priorities(self, priorities: BranchPriorities) -> None: - from gurobipy import GRB - - for (varname, priority) in priorities.items(): - if priority is None: - continue - var = self._varname_to_var[varname] - gvar = self._pyomo_solver._pyomo_var_to_solver_var_map[var] - gvar.setAttr(GRB.Attr.BranchPriority, int(round(priority))) - @overrides def _extract_node_count(self, log: str) -> int: return max(1, int(self._pyomo_solver._solver_model.getAttr("NodeCount"))) diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index b0692c0..5732786 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -2,9 +2,9 @@ # 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, Dict, List +from typing import Any, List -from miplearn.features import Constraint, VariableFeatures, ConstraintFeatures +from miplearn.features import VariableFeatures, ConstraintFeatures from miplearn.solvers.internal import InternalSolver inf = float("inf") @@ -13,14 +13,6 @@ inf = float("inf") # This file is in the main source folder, so that it can be called from Julia. -def _round_constraints(constraints: Dict[str, Constraint]) -> Dict[str, Constraint]: - for (cname, c) in constraints.items(): - for attr in ["slack", "dual_value"]: - if getattr(c, attr) is not None: - setattr(c, attr, round(getattr(c, attr), 6)) - return constraints - - def _round(obj: Any) -> Any: if obj is None: return None @@ -46,20 +38,6 @@ def _filter_attrs(allowed_keys: List[str], obj: Any) -> Any: return obj -def _remove_unsupported_constr_attrs( - solver: InternalSolver, - constraints: Dict[str, Constraint], -) -> Dict[str, Constraint]: - for (cname, c) in constraints.items(): - to_remove = [] - for k in c.__dict__.keys(): - if k not in solver.get_constraint_attrs(): - to_remove.append(k) - for k in to_remove: - setattr(c, k, None) - return constraints - - def run_internal_solver_tests(solver: InternalSolver) -> None: run_basic_usage_tests(solver.clone()) run_warm_start_tests(solver.clone()) diff --git a/miplearn/types.py b/miplearn/types.py index bcc98a4..ca1cfc4 100644 --- a/miplearn/types.py +++ b/miplearn/types.py @@ -10,7 +10,6 @@ if TYPE_CHECKING: # noinspection PyUnresolvedReferences from miplearn.solvers.learning import InternalSolver -BranchPriorities = Dict[str, Optional[float]] Category = Hashable IterationCallback = Callable[[], bool] LazyCallback = Callable[[Any, Any], None] diff --git a/tests/components/test_static_lazy.py b/tests/components/test_static_lazy.py index d8bc3b4..3231bd8 100644 --- a/tests/components/test_static_lazy.py +++ b/tests/components/test_static_lazy.py @@ -14,7 +14,6 @@ from miplearn.components.static_lazy import StaticLazyConstraintsComponent from miplearn.features import ( InstanceFeatures, Features, - Constraint, Sample, ConstraintFeatures, ) diff --git a/tests/solvers/test_internal_solver.py b/tests/solvers/test_internal_solver.py index 33de92e..4d50a98 100644 --- a/tests/solvers/test_internal_solver.py +++ b/tests/solvers/test_internal_solver.py @@ -35,22 +35,3 @@ def test_gurobi_pyomo_solver() -> None: def test_gurobi_solver() -> None: run_internal_solver_tests(GurobiSolver()) - - -def test_redundancy() -> None: - solver = GurobiSolver() - instance = solver.build_test_instance_redundancy() - solver.set_instance(instance) - stats = solver.solve_lp() - assert stats.lp_value == 1.0 - constraints = solver.get_constraints() - assert constraints.names[0] == "c1" - assert constraints.slacks[0] == 0.0 - - solver.relax_constraints(["c1"]) - stats = solver.solve_lp() - assert stats.lp_value == 2.0 - - solver.enforce_constraints(["c1"]) - stats = solver.solve_lp() - assert stats.lp_value == 1.0 diff --git a/tests/test_features.py b/tests/test_features.py index c805f80..bd9ded9 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -5,14 +5,12 @@ from miplearn.features import ( FeaturesExtractor, InstanceFeatures, - Constraint, VariableFeatures, ConstraintFeatures, ) from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.tests import ( assert_equals, - _round_constraints, _round, )