From 4b8672870ab5393c24e7627a2062e3763eda656c Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 19 Jan 2021 22:27:57 -0600 Subject: [PATCH] Add XpressPyomoSolver --- .github/workflows/test.yml | 3 +- docs/customization.md | 22 ++++++++++-- miplearn/solvers/pyomo/base.py | 23 ++++++++----- miplearn/solvers/pyomo/xpress.py | 34 +++++++++++++++++++ miplearn/solvers/tests/__init__.py | 3 +- .../solvers/tests/test_internal_solver.py | 7 ++-- 6 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 miplearn/solvers/pyomo/xpress.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99c99d1..9ad79d8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8] steps: - name: Check out source code @@ -19,6 +19,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + python -m pip install xpress python -m pip install -i https://pypi.gurobi.com gurobipy pip install -r requirements.txt diff --git a/docs/customization.md b/docs/customization.md index 657d4cc..0a26ac9 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -4,9 +4,11 @@ ### Selecting the internal MIP solver -By default, `LearningSolver` uses [Gurobi](https://www.gurobi.com/) as its internal MIP solver, and expects models to be provided using the Pyomo modeling language. Other supported solvers and modeling languages include: +By default, `LearningSolver` uses [Gurobi](https://www.gurobi.com/) as its internal MIP solver, and expects models to be provided using the Pyomo modeling language. Supported solvers and modeling languages include: -* `CplexPyomoSolver`: [IBM ILOG CPLEX](https://www.ibm.com/products/ilog-cplex-optimization-studio) with Pyomo. +* `GurobiPyomoSolver`: Gurobi with Pyomo (default). +* `CplexPyomoSolver`: [IBM ILOG CPLEX](https://www.ibm.com/products/ilog-cplex-optimization-studio) with Pyomo. +* `XpressPyomoSolver`: [FICO XPRESS Solver](https://www.fico.com/en/products/fico-xpress-solver) with Pyomo. * `GurobiSolver`: Gurobi without any modeling language. To switch between solvers, provide the desired class using the `solver` argument: @@ -16,6 +18,22 @@ from miplearn import LearningSolver, CplexPyomoSolver solver = LearningSolver(solver=CplexPyomoSolver) ``` +To configure a particular solver, use the `params` constructor argument, as shown below. + +```python +from miplearn import LearningSolver, GurobiPyomoSolver +solver = LearningSolver( + solver=lambda: GurobiPyomoSolver( + params={ + "TimeLimit": 900, + "MIPGap": 1e-3, + "NodeLimit": 1000, + } + ), +) +``` + + ## Customizing solver components `LearningSolver` is composed by a number of individual machine-learning components, each targeting a different part of the solution process. Each component can be individually enabled, disabled or customized. The following components are enabled by default: diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index 82a204a..b886b1e 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -181,18 +181,27 @@ class BasePyomoSolver(InternalSolver): if not should_repeat: break log = streams[0].getvalue() - return { + stats = { "Lower bound": results["Problem"][0]["Lower bound"], "Upper bound": results["Problem"][0]["Upper bound"], "Wallclock time": total_wallclock_time, - "Nodes": self._extract_node_count(log), "Sense": self._obj_sense, "Log": log, - "Warm start value": self._extract_warm_start_value(log), } + node_count = self._extract_node_count(log) + if node_count is not None: + stats["Nodes"] = node_count + + ws_value = self._extract_warm_start_value(log) + if ws_value is not None: + stats["Warm start value"] = ws_value + + return stats @staticmethod def __extract(log, regexp, default=None): + if regexp is None: + return default value = default for line in log.splitlines(): matches = re.findall(regexp, line) @@ -208,18 +217,16 @@ class BasePyomoSolver(InternalSolver): return value def _extract_node_count(self, log): - return int(self.__extract(log, self._get_node_count_regexp(), default=1)) + return self.__extract(log, self._get_node_count_regexp()) def get_constraint_ids(self): return list(self._cname_to_constr.keys()) - @abstractmethod def _get_warm_start_regexp(self): - pass + return None - @abstractmethod def _get_node_count_regexp(self): - pass + return None def extract_constraint(self, cid): raise Exception("Not implemented") diff --git a/miplearn/solvers/pyomo/xpress.py b/miplearn/solvers/pyomo/xpress.py new file mode 100644 index 0000000..d182577 --- /dev/null +++ b/miplearn/solvers/pyomo/xpress.py @@ -0,0 +1,34 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +import sys +import logging +from io import StringIO +from pyomo import environ as pe +from scipy.stats import randint + +from .base import BasePyomoSolver +from .. import RedirectOutput + +logger = logging.getLogger(__name__) + + +class XpressPyomoSolver(BasePyomoSolver): + """ + An InternalSolver that uses XPRESS and the Pyomo modeling language. + + Parameters + ---------- + params: dict + Dictionary of options to pass to the Pyomo solver. For example, + {"Threads": 4} to set the number of threads. + """ + + def __init__(self, params=None): + super().__init__( + solver_factory=pe.SolverFactory("xpress_persistent"), + params={ + "randomseed": randint(low=0, high=1000).rvs(), + }, + ) diff --git a/miplearn/solvers/tests/__init__.py b/miplearn/solvers/tests/__init__.py index 456f8fc..7d1fdb7 100644 --- a/miplearn/solvers/tests/__init__.py +++ b/miplearn/solvers/tests/__init__.py @@ -5,6 +5,7 @@ from inspect import isclass from miplearn import BasePyomoSolver, GurobiSolver, GurobiPyomoSolver from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance +from miplearn.solvers.pyomo.xpress import XpressPyomoSolver def _get_instance(solver): @@ -31,4 +32,4 @@ def _get_instance(solver): def _get_internal_solvers(): - return [GurobiPyomoSolver, GurobiSolver] + return [GurobiPyomoSolver, GurobiSolver, XpressPyomoSolver] diff --git a/miplearn/solvers/tests/test_internal_solver.py b/miplearn/solvers/tests/test_internal_solver.py index bde466d..07e8a56 100644 --- a/miplearn/solvers/tests/test_internal_solver.py +++ b/miplearn/solvers/tests/test_internal_solver.py @@ -43,7 +43,8 @@ def test_internal_solver_warm_starts(): } ) stats = solver.solve(tee=True) - assert stats["Warm start value"] == 725.0 + if "Warm start value" in stats: + assert stats["Warm start value"] == 725.0 solver.set_warm_start( { @@ -56,7 +57,8 @@ def test_internal_solver_warm_starts(): } ) stats = solver.solve(tee=True) - assert stats["Warm start value"] is None + if "Warm start value" in stats: + assert stats["Warm start value"] is None solver.fix( { @@ -97,7 +99,6 @@ def test_internal_solver(): assert stats["Upper bound"] == 1183.0 assert stats["Sense"] == "max" assert isinstance(stats["Wallclock time"], float) - assert isinstance(stats["Nodes"], int) solution = solver.get_solution() assert solution["x"][0] == 1.0