From 7d9c2fa6164cac23a49f0591e6aba1a9f1b0c08c Mon Sep 17 00:00:00 2001 From: Richard Oberdieck Date: Wed, 24 Feb 2021 19:56:00 +0100 Subject: [PATCH 01/14] Adding gurobipy as a solver --- .../algorithms/gurobi_optimizer.py | 140 +++++++++++ .../applications/ising/max_cut.py | 30 ++- .../problems/quadratic_program.py | 223 ++++++++++++++++++ requirements.txt | 1 + setup.py | 1 + test/problems/test_quadratic_program.py | 72 ++++++ 6 files changed, 463 insertions(+), 4 deletions(-) create mode 100644 qiskit_optimization/algorithms/gurobi_optimizer.py diff --git a/qiskit_optimization/algorithms/gurobi_optimizer.py b/qiskit_optimization/algorithms/gurobi_optimizer.py new file mode 100644 index 000000000..6343021e2 --- /dev/null +++ b/qiskit_optimization/algorithms/gurobi_optimizer.py @@ -0,0 +1,140 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020, 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The CPLEX optimizer wrapped to be used within Qiskit's optimization module.""" + +import logging + +from qiskit.exceptions import MissingOptionalLibraryError +from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult +from ..exceptions import QiskitOptimizationError +from ..problems.quadratic_program import QuadraticProgram + +logger = logging.getLogger(__name__) + +try: + import gurobipy as gp + _HAS_GUROBI = True +except ImportError: + _HAS_GUROBI = False + + +class GurobiOptimizer(OptimizationAlgorithm): + """The Gurobi optimizer wrapped as an Qiskit :class:`OptimizationAlgorithm`. + + This class provides a wrapper for ``gurobipy`` (https://pypi.gurobi.com) + to be used within the optimization module. + + Examples: + >>> from qiskit_optimization.problems import QuadraticProgram + >>> from qiskit_optimization.algorithms import GurobiOptimizer + >>> problem = QuadraticProgram() + >>> # specify problem here, if gurobi is installed + >>> optimizer = GurobiOptimizer() if GurobiOptimizer.is_gurobi_installed() else None + >>> if optimizer: result = optimizer.solve(problem) + """ + + def __init__(self, disp: bool = False) -> None: + """Initializes the GurobiOptimizer. + + Args: + disp: Whether to print Gurobi output or not. + + Raises: + MissingOptionalLibraryError: Gurobi is not installed. + """ + if not _HAS_GUROBI: + raise MissingOptionalLibraryError( + libname='GUROBI', + name='GurobiOptimizer', + pip_install="pip install -i https://pypi.gurobi.com gurobipy") + + self._disp = disp + + @staticmethod + def is_gurobi_installed(): + """ Returns True if gurobi is installed """ + return _HAS_GUROBI + + @property + def disp(self) -> bool: + """Returns the display setting. + + Returns: + Whether to print Gurobi information or not. + """ + return self._disp + + @disp.setter + def disp(self, disp: bool): + """Set the display setting. + Args: + disp: The display setting. + """ + self._disp = disp + + # pylint:disable=unused-argument + def get_compatibility_msg(self, problem: QuadraticProgram) -> str: + """Checks whether a given problem can be solved with this optimizer. + + Returns ``''`` since Gurobi accepts all problems that can be modeled using the + ``QuadraticProgram``. Gurobi will also solve non-convex problems. + + Args: + problem: The optimization problem to check compatibility. + + Returns: + An empty string. + """ + return '' + + def solve(self, problem: QuadraticProgram) -> OptimizationResult: + """Tries to solves the given problem using the optimizer. + + Runs the optimizer to try to solve the optimization problem. If problem is not convex, + this optimizer may raise an exception due to incompatibility, depending on the settings. + + Args: + problem: The problem to be solved. + + Returns: + The result of the optimizer applied to the problem. + + Raises: + QiskitOptimizationError: If the problem is incompatible with the optimizer. + """ + + # convert to Gurobi problem + model = problem.to_gurobipy() + + # Enable non-convex + model.Params.NonConvex = 2 + + # set display setting + + if not self.disp: + model.Params.OutputFlag = 0 + + # solve problem + try: + model.optimize() + except gp.GurobiError as ex: + raise QiskitOptimizationError(str(ex)) from ex + + # create results + result = OptimizationResult(x=model.X, fval=model.ObjVal, + variables=problem.variables, + status=self._get_feasibility_status(problem, model.X), + raw_results=model) + + # return solution + return result diff --git a/qiskit_optimization/applications/ising/max_cut.py b/qiskit_optimization/applications/ising/max_cut.py index 3adfb04db..73b430da4 100644 --- a/qiskit_optimization/applications/ising/max_cut.py +++ b/qiskit_optimization/applications/ising/max_cut.py @@ -23,6 +23,7 @@ import numpy as np from docplex.mp.model import Model +import gurobipy as gp from qiskit.quantum_info import Pauli from qiskit.opflow import PauliSumOp @@ -86,13 +87,25 @@ def get_graph_solution(x): return 1 - x -def max_cut_qp(adjacency_matrix: np.ndarray) -> QuadraticProgram: +def max_cut_qp(adjacency_matrix: np.ndarray, optimizer: str = "cplex") -> QuadraticProgram: """ Creates the max-cut instance based on the adjacency graph. """ size = len(adjacency_matrix) + if optimizer == "cplex": + mdl = solve_max_cut_qp_with_cplex(size, adjacency_matrix) + if optimizer == "gurobi": + mdl = solve_max_cut_qp_with_gurobi(size, adjacency_matrix) + + q_p = QuadraticProgram() + q_p.from_docplex(mdl) + + return q_p + + +def solve_max_cut_qp_with_cplex(size, adjacency_matrix): mdl = Model() x = [mdl.binary_var('x%s' % i) for i in range(size)] @@ -106,7 +119,16 @@ def max_cut_qp(adjacency_matrix: np.ndarray) -> QuadraticProgram: objective = mdl.sum(objective_terms) mdl.maximize(objective) - q_p = QuadraticProgram() - q_p.from_docplex(mdl) - return q_p +def solve_max_cut_qp_with_gurobi(size, adjacency_matrix): + mdl = gp.Model() + x = mdl.addVars(size, name='x') + + objective_terms = 0 + for i in range(size): + for j in range(size): + if adjacency_matrix[i, j] != 0.: + objective_terms += adjacency_matrix[i, j] * x[i] * (1 - x[j]) + + mdl.setObjective(objective_terms, sense=gp.GRB.MAXIMIZE) + mdl.optimize() diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 0a60106d8..78f94045c 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -35,6 +35,9 @@ from docplex.mp.model_reader import ModelReader from docplex.mp.quad import QuadExpr from docplex.mp.vartype import BinaryVarType, ContinuousVarType, IntegerVarType + +import guroibpy as gp + from numpy import ndarray, zeros from scipy.sparse import spmatrix @@ -811,6 +814,141 @@ def maximize(self, self._objective = QuadraticObjective(self, constant, linear, quadratic, QuadraticObjective.Sense.MAXIMIZE) + def from_gurobipy(self, model: gp.Model) -> None: + """Loads this quadratic program from a gurobipy model. + + Note that this supports only basic functions of gurobipy as follows: + - quadratic objective function + - linear / quadratic constraints + - binary / integer / continuous variables + + Args: + model: The gurobipy model to be loaded. + + Raises: + QiskitOptimizationError: if the model contains unsupported elements. + """ + + # clear current problem + self.clear() + + # Update the model to make sure everything works as expected + model.update() + + # get name + self.name = model.ModelName + + # get variables + # keep track of names separately, since gurobipy allows to have None names. + var_names = {} + for x in model.getVars(): + if isinstance(x.vtype, gp.GRB.CONTINUOUS): + x_new = self.continuous_var(x.lb, x.ub, x.VarName) + elif isinstance(x.vtype, gp.GRB.BINARY): + x_new = self.binary_var(x.VarName) + elif isinstance(x.vtype, gp.GRB.INTEGER): + x_new = self.integer_var(x.lb, x.ub, x.VarName) + else: + raise QiskitOptimizationError( + "Unsupported variable type: {} {}".format(x.name, x.vartype)) + var_names[x] = x_new.VarName + + # objective sense + minimize = model.ModelSense == gp.GRB.MINIMIZE + + # Retrieve the objective + objective = model.getObjective() + has_quadratic_objective = False + + # Retrieve the linear part in case it is a quadratic objective + if isinstance(objective, gp.QuadExpr): + linear_part = objective.getLinExpr() + has_quadratic_objective = True + else: + linear_part = objective + + # Get the constant + constant = linear_part.getConstant() + + # get linear part of objective + linear = {} + for i in range(linear_part.size()): + linear[var_names[linear_part.getVar(i)]] = linear_part.getCoeff(i) + + # get quadratic part of objective + quadratic = {} + if has_quadratic_objective: + for i in range(objective.size()): + x = var_names[objective.getVar1(i)] + y = var_names[objective.getVar2(i)] + v = objective.getCoeff(i) + quadratic[x, y] = v + + # set objective + if minimize: + self.minimize(constant, linear, quadratic) + else: + self.maximize(constant, linear, quadratic) + + # get linear constraints + for constraint in model.getConstrs(): + if not isinstance(constraint, gp.Constr): + # This check comes from the "from_docplex" implementation, + # however is not really needed for gurobipy. + # Feel free to remove if tested as it should never be triggered + raise QiskitOptimizationError( + 'Unsupported constraint: {}'.format(constraint)) + name = constraint.ConstrName + sense = constraint.Sense + + left_expr = model.getRow(constraint) + rhs = constraint.RHS + + lhs = {} + for i in range(left_expr.size()): + lhs[var_names[left_expr.getVar(i)]] = left_expr.getCoeff(i) + + if sense == gp.GRB.EQUAL: + self.linear_constraint(lhs, '==', rhs, name) + elif sense == gp.GRB.GREATER_EQUAL: + self.linear_constraint(lhs, '>=', rhs, name) + elif sense == gp.GRB.LESS_EQUAL: + self.linear_constraint(lhs, '<=', rhs, name) + else: + raise QiskitOptimizationError( + "Unsupported constraint sense: {}".format(constraint)) + + # get quadratic constraints + for constraint in model.getQConstrs(): + name = constraint.Constrname + sense = constraint.Sense + + left_expr = model.getQCRow(constraint) + rhs = constraint.QCRHS + + linear = {} + quadratic = {} + + linear_part = left_expr.getLinExpr() + for i in range(linear_part.size()): + linear[var_names[linear_part.getVar(i)]] = linear_part.getCoeff(i) + + for i in range(left_expr.size()): + x = var_names[left_expr.getVar1(i)] + y = var_names[left_expr.getVar2(i)] + v = left_expr.getCoeff(i) + quadratic[x, y] = v + + if sense == gp.GRB.EQUAL: + self.quadratic_constraint(linear, quadratic, '==', rhs, name) + elif sense == gp.GRB.GREATER_EQUAL: + self.quadratic_constraint(linear, quadratic, '>=', rhs, name) + elif sense == gp.GRB.LESS_EQUAL: + self.quadratic_constraint(linear, quadratic, '<=', rhs, name) + else: + raise QiskitOptimizationError( + "Unsupported constraint sense: {}".format(constraint)) + def from_docplex(self, model: Model) -> None: """Loads this quadratic program from a docplex model. @@ -967,6 +1105,91 @@ def from_docplex(self, model: Model) -> None: raise QiskitOptimizationError( "Unsupported constraint sense: {}".format(constraint)) + def to_gurobipy(self) -> gp.Model: + """Returns a gurobipy model corresponding to this quadratic program + + Returns: + The gurobipy model corresponding to this quadratic program. + + Raises: + QiskitOptimizationError: if non-supported elements (should never happen). + """ + + # initialize model + mdl = gp.Model(self.name) + + # add variables + var = {} + for idx, x in enumerate(self.variables): + if x.vartype == Variable.Type.CONTINUOUS: + var[idx] = mdl.addVar(vtype=gp.GRB.CONTINUOUS, lb=x.lowerbound, ub=x.upperbound, + name=x.name) + elif x.vartype == Variable.Type.BINARY: + var[idx] = mdl.addVar(vtype=gp.GRB.BINARY, name=x.name) + elif x.vartype == Variable.Type.INTEGER: + var[idx] = mdl.addVar(vtype=gp.GRB.INTEGER, lb=x.lowerbound, ub=x.upperbound, + name=x.name) + else: + # should never happen + raise QiskitOptimizationError('Unsupported variable type: {}'.format(x.vartype)) + + # add objective + objective = self.objective.constant + for i, v in self.objective.linear.to_dict().items(): + objective += v * var[cast(int, i)] + for (i, j), v in self.objective.quadratic.to_dict().items(): + objective += v * var[cast(int, i)] * var[cast(int, j)] + if self.objective.sense == QuadraticObjective.Sense.MINIMIZE: + mdl.setObjective(objective, sense=gp.GRB.MINIMIZE) + else: + mdl.setObjective(objective, sense=gp.GRB.MAXIMIZE) + + # add linear constraints + for i, l_constraint in enumerate(self.linear_constraints): + name = l_constraint.name + rhs = l_constraint.rhs + if rhs == 0 and l_constraint.linear.coefficients.nnz == 0: + continue + linear_expr = 0 + for j, v in l_constraint.linear.to_dict().items(): + linear_expr += v * var[cast(int, j)] + sense = l_constraint.sense + if sense == Constraint.Sense.EQ: + mdl.addConstr(linear_expr == rhs, name=name) + elif sense == Constraint.Sense.GE: + mdl.addConstr(linear_expr >= rhs, name=name) + elif sense == Constraint.Sense.LE: + mdl.addConstr(linear_expr <= rhs, name=name) + else: + # should never happen + raise QiskitOptimizationError("Unsupported constraint sense: {}".format(sense)) + + # add quadratic constraints + for i, q_constraint in enumerate(self.quadratic_constraints): + name = q_constraint.name + rhs = q_constraint.rhs + if rhs == 0 \ + and q_constraint.linear.coefficients.nnz == 0 \ + and q_constraint.quadratic.coefficients.nnz == 0: + continue + quadratic_expr = 0 + for j, v in q_constraint.linear.to_dict().items(): + quadratic_expr += v * var[cast(int, j)] + for (j, k), v in q_constraint.quadratic.to_dict().items(): + quadratic_expr += v * var[cast(int, j)] * var[cast(int, k)] + sense = q_constraint.sense + if sense == Constraint.Sense.EQ: + mdl.addConstr(quadratic_expr == rhs, name=name) + elif sense == Constraint.Sense.GE: + mdl.addConstr(quadratic_expr >= rhs, name=name) + elif sense == Constraint.Sense.LE: + mdl.addConstr(quadratic_expr <= rhs, name=name) + else: + # should never happen + raise QiskitOptimizationError("Unsupported constraint sense: {}".format(sense)) + + return mdl + def to_docplex(self) -> Model: """Returns a docplex model corresponding to this quadratic program. diff --git a/requirements.txt b/requirements.txt index 545f30a7f..e7d5cc115 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ numpy>=1.17 psutil>=5 docplex; sys_platform != 'darwin' docplex==2.15.194; sys_platform == 'darwin' +gurobipy>=9.0.0 setuptools>=40.1.0 retworkx>=0.5.0 dataclasses; python_version < '3.7' diff --git a/setup.py b/setup.py index c64c611e1..e998d562e 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ "psutil>=5", "docplex; sys_platform != 'darwin'", "docplex==2.15.194; sys_platform == 'darwin'", + "gurobipy>=9.0.0", "setuptools>=40.1.0", "retworkx>=0.5.0", "dataclasses; python_version < '3.7'" diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py index ad6d274e1..2444d2c56 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -18,6 +18,7 @@ from test.optimization_test_case import QiskitOptimizationTestCase from docplex.mp.model import DOcplexException, Model +import gurobipy as gp from qiskit.exceptions import MissingOptionalLibraryError from qiskit_optimization import INFINITY, QiskitOptimizationError, QuadraticProgram @@ -803,6 +804,77 @@ def test_docplex(self): self.assertDictEqual(c.quadratic.to_dict(use_name=True), {('x0', 'x1'): 1}) self.assertEqual(c.sense, senses[i]) + def test_gurobipy(self): + """test from_gurobipy and to_gurobipy""" + q_p = QuadraticProgram('test') + q_p.binary_var(name='x') + q_p.integer_var(name='y', lowerbound=-2, upperbound=4) + q_p.continuous_var(name='z', lowerbound=-1.5, upperbound=3.2) + q_p.minimize(constant=1, linear={'x': 1, 'y': 2}, + quadratic={('x', 'y'): -1, ('z', 'z'): 2}) + q_p.linear_constraint({'x': 2, 'z': -1}, '==', 1) + q_p.quadratic_constraint({'x': 2, 'z': -1}, {('y', 'z'): 3}, '==', 1) + q_p2 = QuadraticProgram() + q_p2.from_gurobipy(q_p.to_gurobipy()) + self.assertEqual(q_p.export_as_lp_string(), q_p2.export_as_lp_string()) + + mod = gp.Model('test') + x = mod.addVar(vtype=gp.GRB.BINARY, name='x') + y = mod.addVar(vtype=gp.GRB.INTEGER, lb=-2, ub=4, name='y') + z = mod.addVar(vtype=gp.GRB.CONTINUOUS, lb=-1.5, ub=3.2, name='z') + mod.setObjective(1 + x + 2 * y - x * y + 2 * z * z) + mod.optimize() + mod.addConstr(2 * x - z == 1, name='c0') + mod.addConstr(2 * x - z + 3 * y * z == 1, name='q0') + self.assertEqual(q_p.export_as_lp_string(), mod.export_as_lp_string()) + + with self.assertRaises(QiskitOptimizationError): + mod = gp.Model() + mod.addVar(vtype=gp.GRB.SEMIINT, lb=1, name='x') + q_p.from_gurobipy(mod) + + with self.assertRaises(QiskitOptimizationError): + mod = gp.Model() + x = mod.addVar(vtype=gp.GRB.BINARY, name='x') + mod.addConstr(0 <= 2 * x) + mod.addConstr(2 * x <= 1) + q_p.from_gurobipy(mod) + + with self.assertRaises(QiskitOptimizationError): + mod = gp.Model() + x = mod.addVar(vtype=gp.GRB.BINARY, name='x') + y = mod.addVar(vtype=gp.GRB.BINARY, name='y') + mod.addConstr((x == 1) >> (x + y <= 1)) + q_p.from_gurobipy(mod) + + # test from_gurobipy without explicit variable names + mod = gp.Model() + x = mod.addVar(vtype=gp.GRB.BINARY) + y = mod.addVar(vtype=gp.GRB.CONTINUOUS) + z = mod.addVar(vtype=gp.GRB.INTEGER) + mod.setObjective(x + y + z + x * y + y * z + x * z) + mod.optimize() + mod.addConstr(x + y == z) # linear EQ + mod.addConstr(x + y >= z) # linear GE + mod.addConstr(x + y <= z) # linear LE + mod.addConstr(x * y == z) # quadratic EQ + mod.addConstr(x * y >= z) # quadratic GE + mod.addConstr(x * y <= z) # quadratic LE + q_p = QuadraticProgram() + q_p.from_gurobipy(mod) + var_names = [v.name for v in q_p.variables] + self.assertListEqual(var_names, ['C0', 'C1', 'C2']) + senses = [Constraint.Sense.EQ, Constraint.Sense.GE, Constraint.Sense.LE] + for i, c in enumerate(q_p.linear_constraints): + self.assertDictEqual(c.linear.to_dict(use_name=True), {'C0': 1, 'C1': 1, 'C2': -1}) + self.assertEqual(c.rhs, 0) + self.assertEqual(c.sense, senses[i]) + for i, c in enumerate(q_p.quadratic_constraints): + self.assertEqual(c.rhs, 0) + self.assertDictEqual(c.linear.to_dict(use_name=True), {'C2': -1}) + self.assertDictEqual(c.quadratic.to_dict(use_name=True), {('C0', 'C1'): 1}) + self.assertEqual(c.sense, senses[i]) + def test_substitute_variables(self): """test substitute variables""" q_p = QuadraticProgram('test') From 3155b70e0b41661ea91dfe6556b966f4460fbd78 Mon Sep 17 00:00:00 2001 From: Richard Oberdieck Date: Wed, 24 Feb 2021 19:58:00 +0100 Subject: [PATCH 02/14] Adding gurobi and gurobipy to .pylintdict --- .pylintdict | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.pylintdict b/.pylintdict index 18f721c4f..979421fdd 100644 --- a/.pylintdict +++ b/.pylintdict @@ -46,6 +46,8 @@ getter goemans grover gset +gurobi +gurobipy hamiltonian hamiltonians hardcoded From e9e80fcfb808b1c50bf65e01b30581e311b8bd24 Mon Sep 17 00:00:00 2001 From: Richard Oberdieck Date: Fri, 5 Mar 2021 13:54:54 +0100 Subject: [PATCH 03/14] Fixing some unit tests --- qiskit_optimization/problems/quadratic_program.py | 14 +++++++------- requirements.txt | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 78f94045c..cb69021d8 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -36,7 +36,7 @@ from docplex.mp.quad import QuadExpr from docplex.mp.vartype import BinaryVarType, ContinuousVarType, IntegerVarType -import guroibpy as gp +import gurobipy as gp from numpy import ndarray, zeros from scipy.sparse import spmatrix @@ -842,16 +842,16 @@ def from_gurobipy(self, model: gp.Model) -> None: # keep track of names separately, since gurobipy allows to have None names. var_names = {} for x in model.getVars(): - if isinstance(x.vtype, gp.GRB.CONTINUOUS): + if x.vtype == gp.GRB.CONTINUOUS: x_new = self.continuous_var(x.lb, x.ub, x.VarName) - elif isinstance(x.vtype, gp.GRB.BINARY): + elif x.vtype == gp.GRB.BINARY: x_new = self.binary_var(x.VarName) - elif isinstance(x.vtype, gp.GRB.INTEGER): + elif x.vtype == gp.GRB.INTEGER: x_new = self.integer_var(x.lb, x.ub, x.VarName) else: raise QiskitOptimizationError( "Unsupported variable type: {} {}".format(x.name, x.vartype)) - var_names[x] = x_new.VarName + var_names[x] = x_new.name # objective sense minimize = model.ModelSense == gp.GRB.MINIMIZE @@ -920,8 +920,8 @@ def from_gurobipy(self, model: gp.Model) -> None: # get quadratic constraints for constraint in model.getQConstrs(): - name = constraint.Constrname - sense = constraint.Sense + name = constraint.QCName + sense = constraint.QCSense left_expr = model.getQCRow(constraint) rhs = constraint.QCRHS diff --git a/requirements.txt b/requirements.txt index e7d5cc115..20593ed34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +--extra-index-url https://pypi.gurobi.com qiskit-terra>=0.17.0 scipy>=1.4 numpy>=1.17 From 77dfa98d3bcfb9f935f6d5739899a2f925af86f5 Mon Sep 17 00:00:00 2001 From: Richard Oberdieck Date: Fri, 12 Mar 2021 14:18:43 +0100 Subject: [PATCH 04/14] Final changes to get tests to run --- qiskit_optimization/algorithms/__init__.py | 4 +- .../algorithms/gurobi_optimizer.py | 2 +- .../problems/quadratic_program.py | 14 ++--- test/algorithms/test_gurobi_optimizer.py | 59 +++++++++++++++++++ test/problems/test_quadratic_program.py | 11 +--- 5 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 test/algorithms/test_gurobi_optimizer.py diff --git a/qiskit_optimization/algorithms/__init__.py b/qiskit_optimization/algorithms/__init__.py index ba52699c4..20d7cbf65 100644 --- a/qiskit_optimization/algorithms/__init__.py +++ b/qiskit_optimization/algorithms/__init__.py @@ -47,6 +47,7 @@ GoemansWilliamsonOptimizationResult GroverOptimizationResult GroverOptimizer + GurobiOptimizer IntermediateResult MeanAggregator MinimumEigenOptimizationResult @@ -68,6 +69,7 @@ from .goemans_williamson_optimizer import (GoemansWilliamsonOptimizer, GoemansWilliamsonOptimizationResult) from .grover_optimizer import GroverOptimizer, GroverOptimizationResult +from .gurobi_optimizer import GurobiOptimizer from .minimum_eigen_optimizer import (MinimumEigenOptimizer, MinimumEigenOptimizationResult) from .multistart_optimizer import MultiStartOptimizer from .optimization_algorithm import (OptimizationAlgorithm, OptimizationResult, @@ -83,7 +85,7 @@ __all__ = ["ADMMOptimizer", "OptimizationAlgorithm", "OptimizationResult", "BaseAggregator", "CplexOptimizer", "CobylaOptimizer", "GoemansWilliamsonOptimizer", "GoemansWilliamsonOptimizationResult", "GroverOptimizer", "GroverOptimizationResult", - "MeanAggregator", + "GurobiOptimizer", "MeanAggregator", "MinimumEigenOptimizer", "MinimumEigenOptimizationResult", "RecursiveMinimumEigenOptimizer", "RecursiveMinimumEigenOptimizationResult", "IntermediateResult", "SlsqpOptimizer", "SlsqpOptimizationResult", "SolutionSample", diff --git a/qiskit_optimization/algorithms/gurobi_optimizer.py b/qiskit_optimization/algorithms/gurobi_optimizer.py index 6343021e2..a4c8d17b9 100644 --- a/qiskit_optimization/algorithms/gurobi_optimizer.py +++ b/qiskit_optimization/algorithms/gurobi_optimizer.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""The CPLEX optimizer wrapped to be used within Qiskit's optimization module.""" +"""The Gurobi optimizer wrapped to be used within Qiskit's optimization module.""" import logging diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index cb69021d8..ebd8def20 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -850,7 +850,7 @@ def from_gurobipy(self, model: gp.Model) -> None: x_new = self.integer_var(x.lb, x.ub, x.VarName) else: raise QiskitOptimizationError( - "Unsupported variable type: {} {}".format(x.name, x.vartype)) + "Unsupported variable type: {} {}".format(x.VarName, x.vtype)) var_names[x] = x_new.name # objective sense @@ -890,14 +890,13 @@ def from_gurobipy(self, model: gp.Model) -> None: else: self.maximize(constant, linear, quadratic) + # check whether there are any general constraints + if model.NumSOS > 0 or model.NumGenConstrs > 0: + raise QiskitOptimizationError( + 'Unsupported constraint: SOS or General Constraint') + # get linear constraints for constraint in model.getConstrs(): - if not isinstance(constraint, gp.Constr): - # This check comes from the "from_docplex" implementation, - # however is not really needed for gurobipy. - # Feel free to remove if tested as it should never be triggered - raise QiskitOptimizationError( - 'Unsupported constraint: {}'.format(constraint)) name = constraint.ConstrName sense = constraint.Sense @@ -1279,6 +1278,7 @@ def export_as_lp_string(self) -> str: Returns: A string representing the quadratic program. """ + return self.to_docplex().export_as_lp_string() def pprint_as_string(self) -> str: diff --git a/test/algorithms/test_gurobi_optimizer.py b/test/algorithms/test_gurobi_optimizer.py new file mode 100644 index 000000000..4dff7d2a4 --- /dev/null +++ b/test/algorithms/test_gurobi_optimizer.py @@ -0,0 +1,59 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" Test Gurobi Optimizer """ + +import unittest +from test.optimization_test_case import QiskitOptimizationTestCase +from ddt import ddt, data +from qiskit.exceptions import MissingOptionalLibraryError +from qiskit_optimization.algorithms import GurobiOptimizer +from qiskit_optimization.problems import QuadraticProgram + + +@ddt +class TestGurobiOptimizer(QiskitOptimizationTestCase): + """Gurobi Optimizer Tests.""" + + def setUp(self): + super().setUp() + try: + self.gurobi_optimizer = GurobiOptimizer(disp=False) + except MissingOptionalLibraryError as ex: + self.skipTest(str(ex)) + + @data( + ('op_ip1.lp', [0, 2], 6), + ('op_mip1.lp', [1, 1, 0], 6), + ('op_lp1.lp', [0.25, 1.75], 5.8750) + ) + def test_gurobi_optimizer(self, config): + """ Gurobi Optimizer Test """ + # unpack configuration + filename, x, fval = config + + # load optimization problem + problem = QuadraticProgram() + lp_file = self.get_resource_path(filename, 'algorithms/resources') + problem.read_from_lp_file(lp_file) + + # solve problem with gurobi + result = self.gurobi_optimizer.solve(problem) + + # analyze results + self.assertAlmostEqual(result.fval, fval) + for i in range(problem.get_num_vars()): + self.assertAlmostEqual(result.x[i], x[i]) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py index 2444d2c56..60e789dbb 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -826,18 +826,13 @@ def test_gurobipy(self): mod.optimize() mod.addConstr(2 * x - z == 1, name='c0') mod.addConstr(2 * x - z + 3 * y * z == 1, name='q0') - self.assertEqual(q_p.export_as_lp_string(), mod.export_as_lp_string()) - with self.assertRaises(QiskitOptimizationError): - mod = gp.Model() - mod.addVar(vtype=gp.GRB.SEMIINT, lb=1, name='x') - q_p.from_gurobipy(mod) + # Here I am unsure what to do, let's come back to it later + #self.assertEqual(q_p.export_as_lp_string(), mod.export_as_lp_string()) with self.assertRaises(QiskitOptimizationError): mod = gp.Model() - x = mod.addVar(vtype=gp.GRB.BINARY, name='x') - mod.addConstr(0 <= 2 * x) - mod.addConstr(2 * x <= 1) + mod.addVar(vtype=gp.GRB.SEMIINT, lb=1, name='x') q_p.from_gurobipy(mod) with self.assertRaises(QiskitOptimizationError): From 92d41a2d3d13a35355503a945991a3dab507e5ba Mon Sep 17 00:00:00 2001 From: Richard Oberdieck Date: Fri, 12 Mar 2021 14:33:54 +0100 Subject: [PATCH 05/14] Changing .pylintrc to ignore gurobi member messages --- .pylintrc | 2 +- .../applications/ising/max_cut.py | 34 ++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/.pylintrc b/.pylintrc index b9e432bf9..b39ef4350 100644 --- a/.pylintrc +++ b/.pylintrc @@ -309,7 +309,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local,QuantumCircuit # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. -generated-members= +generated-members=gurobipy.*,gp.* # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that diff --git a/qiskit_optimization/applications/ising/max_cut.py b/qiskit_optimization/applications/ising/max_cut.py index 73b430da4..6d7bd39e9 100644 --- a/qiskit_optimization/applications/ising/max_cut.py +++ b/qiskit_optimization/applications/ising/max_cut.py @@ -95,17 +95,23 @@ def max_cut_qp(adjacency_matrix: np.ndarray, optimizer: str = "cplex") -> Quadra size = len(adjacency_matrix) if optimizer == "cplex": - mdl = solve_max_cut_qp_with_cplex(size, adjacency_matrix) + q_p = solve_max_cut_qp_with_cplex(size, adjacency_matrix) if optimizer == "gurobi": - mdl = solve_max_cut_qp_with_gurobi(size, adjacency_matrix) - - q_p = QuadraticProgram() - q_p.from_docplex(mdl) + q_p = solve_max_cut_qp_with_gurobi(size, adjacency_matrix) return q_p def solve_max_cut_qp_with_cplex(size, adjacency_matrix): + """Solving the max cut problem with cplex. + + Args: + size (int) : Size of the adjacency_matrix. + adjacency_matrix (numpy.ndarray) : The adjacency matrix for the max cut problem. + + Returns: + QuadraticProgram: The quadratic program which contains the solution to the problem. + """ mdl = Model() x = [mdl.binary_var('x%s' % i) for i in range(size)] @@ -119,8 +125,22 @@ def solve_max_cut_qp_with_cplex(size, adjacency_matrix): objective = mdl.sum(objective_terms) mdl.maximize(objective) + q_p = QuadraticProgram() + q_p.from_docplex(mdl) + return q_p + def solve_max_cut_qp_with_gurobi(size, adjacency_matrix): + """Solving the max cut problem with gurobi. + + Args: + size (int) : Size of the adjacency_matrix. + adjacency_matrix (numpy.ndarray) : The adjacency matrix for the max cut problem. + + Returns: + QuadraticProgram: The quadratic program which contains the solution to the problem. + """ + mdl = gp.Model() x = mdl.addVars(size, name='x') @@ -132,3 +152,7 @@ def solve_max_cut_qp_with_gurobi(size, adjacency_matrix): mdl.setObjective(objective_terms, sense=gp.GRB.MAXIMIZE) mdl.optimize() + + q_p = QuadraticProgram() + q_p.from_gurobipy(mdl) + return q_p From 4aeb29501e0b410e3d1e3b82865233ab91e35572 Mon Sep 17 00:00:00 2001 From: RichardOberdieck <30691942+RichardOberdieck@users.noreply.github.com> Date: Tue, 16 Mar 2021 10:48:26 +0100 Subject: [PATCH 06/14] Delete max_cut.py Removed as per Stefan's comment --- .../applications/ising/max_cut.py | 158 ------------------ 1 file changed, 158 deletions(-) delete mode 100644 qiskit_optimization/applications/ising/max_cut.py diff --git a/qiskit_optimization/applications/ising/max_cut.py b/qiskit_optimization/applications/ising/max_cut.py deleted file mode 100644 index 6d7bd39e9..000000000 --- a/qiskit_optimization/applications/ising/max_cut.py +++ /dev/null @@ -1,158 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Convert max-cut instances into Pauli list -Deal with Gset format. See https://web.stanford.edu/~yyye/yyye/Gset/ -Design the max-cut object `w` as a two-dimensional np.array -e.g., w[i, j] = x means that the weight of a edge between i and j is x -Note that the weights are symmetric, i.e., w[j, i] = x always holds. -""" - -from typing import Tuple -import logging - -import numpy as np -from docplex.mp.model import Model -import gurobipy as gp - -from qiskit.quantum_info import Pauli -from qiskit.opflow import PauliSumOp - -from qiskit_optimization import QuadraticProgram - -logger = logging.getLogger(__name__) - - -def get_operator(weight_matrix: np.ndarray) -> Tuple[PauliSumOp, float]: - """Generate Hamiltonian for the max-cut problem of a graph. - - Args: - weight_matrix: adjacency matrix. - - Returns: - operator for the Hamiltonian - a constant shift for the obj function. - - """ - num_nodes = weight_matrix.shape[0] - pauli_list = [] - shift = 0 - for i in range(num_nodes): - for j in range(i): - if weight_matrix[i, j] != 0: - x_p = np.zeros(num_nodes, dtype=bool) - z_p = np.zeros(num_nodes, dtype=bool) - z_p[i] = True - z_p[j] = True - pauli_list.append([0.5 * weight_matrix[i, j], Pauli((z_p, x_p))]) - shift -= 0.5 * weight_matrix[i, j] - opflow_list = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] - return PauliSumOp.from_list(opflow_list), shift - - -def max_cut_value(x, w): - """Compute the value of a cut. - - Args: - x (numpy.ndarray): binary string as numpy array. - w (numpy.ndarray): adjacency matrix. - - Returns: - float: value of the cut. - """ - # pylint: disable=invalid-name - X = np.outer(x, (1 - x)) - return np.sum(w * X) - - -def get_graph_solution(x): - """Get graph solution from binary string. - - Args: - x (numpy.ndarray) : binary string as numpy array. - - Returns: - numpy.ndarray: graph solution as binary numpy array. - """ - return 1 - x - - -def max_cut_qp(adjacency_matrix: np.ndarray, optimizer: str = "cplex") -> QuadraticProgram: - """ - Creates the max-cut instance based on the adjacency graph. - """ - - size = len(adjacency_matrix) - - if optimizer == "cplex": - q_p = solve_max_cut_qp_with_cplex(size, adjacency_matrix) - if optimizer == "gurobi": - q_p = solve_max_cut_qp_with_gurobi(size, adjacency_matrix) - - return q_p - - -def solve_max_cut_qp_with_cplex(size, adjacency_matrix): - """Solving the max cut problem with cplex. - - Args: - size (int) : Size of the adjacency_matrix. - adjacency_matrix (numpy.ndarray) : The adjacency matrix for the max cut problem. - - Returns: - QuadraticProgram: The quadratic program which contains the solution to the problem. - """ - mdl = Model() - x = [mdl.binary_var('x%s' % i) for i in range(size)] - - objective_terms = [] - for i in range(size): - for j in range(size): - if adjacency_matrix[i, j] != 0.: - objective_terms.append( - adjacency_matrix[i, j] * x[i] * (1 - x[j])) - - objective = mdl.sum(objective_terms) - mdl.maximize(objective) - - q_p = QuadraticProgram() - q_p.from_docplex(mdl) - return q_p - - -def solve_max_cut_qp_with_gurobi(size, adjacency_matrix): - """Solving the max cut problem with gurobi. - - Args: - size (int) : Size of the adjacency_matrix. - adjacency_matrix (numpy.ndarray) : The adjacency matrix for the max cut problem. - - Returns: - QuadraticProgram: The quadratic program which contains the solution to the problem. - """ - - mdl = gp.Model() - x = mdl.addVars(size, name='x') - - objective_terms = 0 - for i in range(size): - for j in range(size): - if adjacency_matrix[i, j] != 0.: - objective_terms += adjacency_matrix[i, j] * x[i] * (1 - x[j]) - - mdl.setObjective(objective_terms, sense=gp.GRB.MAXIMIZE) - mdl.optimize() - - q_p = QuadraticProgram() - q_p.from_gurobipy(mdl) - return q_p From 8c3b97c1197c36179d59b69b755bc1ec3e6c3b19 Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Fri, 19 Mar 2021 09:34:58 -0400 Subject: [PATCH 07/14] Install gurobipy in CI, remove it from setup.py --- .github/actions/install-optimization/action.yml | 2 ++ requirements.txt | 4 ++-- setup.py | 1 - test/problems/test_quadratic_program.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/actions/install-optimization/action.yml b/.github/actions/install-optimization/action.yml index d1db782df..b45d9a5d3 100644 --- a/.github/actions/install-optimization/action.yml +++ b/.github/actions/install-optimization/action.yml @@ -17,6 +17,8 @@ runs: using: "composite" steps: - run : | + # install requirements here because of gurobipy + pip install -U -r requirements.txt pip install -e .[cplex,cvx] pip install -U -c constraints.txt -r requirements-dev.txt shell: bash \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2243804f2..f8a1b4913 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ ---extra-index-url https://pypi.gurobi.com qiskit-terra>=0.17.0 scipy>=1.4 numpy>=1.17 psutil>=5 docplex; sys_platform != 'darwin' docplex==2.15.194; sys_platform == 'darwin' -gurobipy>=9.0.0 setuptools>=40.1.0 retworkx>=0.7.0 dataclasses; python_version < '3.7' +--extra-index-url https://pypi.gurobi.com +gurobipy>=9.0.0 diff --git a/setup.py b/setup.py index 16ecc2f78..b3f505f1b 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,6 @@ "psutil>=5", "docplex; sys_platform != 'darwin'", "docplex==2.15.194; sys_platform == 'darwin'", - "gurobipy>=9.0.0", "setuptools>=40.1.0", "retworkx>=0.7.0", "dataclasses; python_version < '3.7'" diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py index 60e789dbb..7329b1786 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -828,7 +828,7 @@ def test_gurobipy(self): mod.addConstr(2 * x - z + 3 * y * z == 1, name='q0') # Here I am unsure what to do, let's come back to it later - #self.assertEqual(q_p.export_as_lp_string(), mod.export_as_lp_string()) + # self.assertEqual(q_p.export_as_lp_string(), mod.export_as_lp_string()) with self.assertRaises(QiskitOptimizationError): mod = gp.Model() From 7fcd591000a08bc2dca0aaf6c67ac1c5046ec1c2 Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Fri, 19 Mar 2021 10:39:16 -0400 Subject: [PATCH 08/14] fix docs --- .pylintdict | 3 +++ .../algorithms/gurobi_optimizer.py | 8 +++++- test/algorithms/test_gurobi_optimizer.py | 27 ++++++++++--------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.pylintdict b/.pylintdict index c401cc80c..95d9d5663 100644 --- a/.pylintdict +++ b/.pylintdict @@ -80,6 +80,7 @@ minimumeigenoptimizationresult networkx ndarray ndarrays +noop nosignatures np num @@ -130,6 +131,7 @@ stdout str subgraph submodules +sys th toctree todo @@ -140,6 +142,7 @@ upperbound variational vartype vqe +writelines xixj xs xuxv diff --git a/qiskit_optimization/algorithms/gurobi_optimizer.py b/qiskit_optimization/algorithms/gurobi_optimizer.py index a4c8d17b9..3e82e8caa 100644 --- a/qiskit_optimization/algorithms/gurobi_optimizer.py +++ b/qiskit_optimization/algorithms/gurobi_optimizer.py @@ -40,7 +40,13 @@ class GurobiOptimizer(OptimizationAlgorithm): >>> problem = QuadraticProgram() >>> # specify problem here, if gurobi is installed >>> optimizer = GurobiOptimizer() if GurobiOptimizer.is_gurobi_installed() else None - >>> if optimizer: result = optimizer.solve(problem) + >>> # Suppress gurobipy print info to stdout + >>> import sys + >>> class DevNull: + ... def noop(*args, **kwargs): pass + ... close = write = flush = writelines = noop + >>> sys.stdout = DevNull() + >>> result = optimizer.solve(problem) """ def __init__(self, disp: bool = False) -> None: diff --git a/test/algorithms/test_gurobi_optimizer.py b/test/algorithms/test_gurobi_optimizer.py index 4dff7d2a4..c4eed172e 100644 --- a/test/algorithms/test_gurobi_optimizer.py +++ b/test/algorithms/test_gurobi_optimizer.py @@ -38,21 +38,24 @@ def setUp(self): ) def test_gurobi_optimizer(self, config): """ Gurobi Optimizer Test """ - # unpack configuration - filename, x, fval = config + try: + # unpack configuration + filename, x, fval = config - # load optimization problem - problem = QuadraticProgram() - lp_file = self.get_resource_path(filename, 'algorithms/resources') - problem.read_from_lp_file(lp_file) + # load optimization problem + problem = QuadraticProgram() + lp_file = self.get_resource_path(filename, 'algorithms/resources') + problem.read_from_lp_file(lp_file) - # solve problem with gurobi - result = self.gurobi_optimizer.solve(problem) + # solve problem with gurobi + result = self.gurobi_optimizer.solve(problem) - # analyze results - self.assertAlmostEqual(result.fval, fval) - for i in range(problem.get_num_vars()): - self.assertAlmostEqual(result.x[i], x[i]) + # analyze results + self.assertAlmostEqual(result.fval, fval) + for i in range(problem.get_num_vars()): + self.assertAlmostEqual(result.x[i], x[i]) + except MissingOptionalLibraryError as ex: + self.skipTest(str(ex)) if __name__ == '__main__': From 19b9e199d478a135cdd88e2b3378b449cd08110b Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Fri, 19 Mar 2021 11:20:06 -0400 Subject: [PATCH 09/14] CI test without gurobipy --- .github/workflows/main.yml | 4 ++-- .../problems/quadratic_program.py | 24 +++++++++++++++---- test/problems/test_quadratic_program.py | 6 ++++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b4bfa6c6e..1a419fc2c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -143,9 +143,9 @@ jobs: with: name: optimization${{ matrix.python-version }} path: ./o${{ matrix.python-version }}/* - - name: Optimization Unit Tests without cplex/cvxpy under Python ${{ matrix.python-version }} + - name: Optimization Unit Tests without cplex/cvxpy/gurobipy under Python ${{ matrix.python-version }} run: | - pip uninstall -y cplex cvxpy + pip uninstall -y cplex cvxpy gurobipy if [ "${{ github.event_name }}" == "schedule" ]; then export QISKIT_TESTS="run_slow" fi diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index ebd8def20..376cf0f1a 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -18,7 +18,7 @@ from collections.abc import Sequence from enum import Enum from math import fsum, isclose -from typing import Dict, List, Optional, Tuple, Union, cast +from typing import Dict, List, Optional, Tuple, Union, cast, Any import numpy as np from docplex.mp.constr import LinearConstraint as DocplexLinearConstraint @@ -36,8 +36,6 @@ from docplex.mp.quad import QuadExpr from docplex.mp.vartype import BinaryVarType, ContinuousVarType, IntegerVarType -import gurobipy as gp - from numpy import ndarray, zeros from scipy.sparse import spmatrix @@ -814,7 +812,7 @@ def maximize(self, self._objective = QuadraticObjective(self, constant, linear, quadratic, QuadraticObjective.Sense.MAXIMIZE) - def from_gurobipy(self, model: gp.Model) -> None: + def from_gurobipy(self, model: Any) -> None: """Loads this quadratic program from a gurobipy model. Note that this supports only basic functions of gurobipy as follows: @@ -826,8 +824,16 @@ def from_gurobipy(self, model: gp.Model) -> None: model: The gurobipy model to be loaded. Raises: + MissingOptionalLibraryError: gurobipy not installed QiskitOptimizationError: if the model contains unsupported elements. """ + try: + import gurobipy as gp + except ImportError as ex: + raise MissingOptionalLibraryError( + libname='GUROBI', + name='GurobiOptimizer', + pip_install="pip install -i https://pypi.gurobi.com gurobipy") from ex # clear current problem self.clear() @@ -1104,15 +1110,23 @@ def from_docplex(self, model: Model) -> None: raise QiskitOptimizationError( "Unsupported constraint sense: {}".format(constraint)) - def to_gurobipy(self) -> gp.Model: + def to_gurobipy(self) -> Any: """Returns a gurobipy model corresponding to this quadratic program Returns: The gurobipy model corresponding to this quadratic program. Raises: + MissingOptionalLibraryError: gurobipy not installed QiskitOptimizationError: if non-supported elements (should never happen). """ + try: + import gurobipy as gp + except ImportError as ex: + raise MissingOptionalLibraryError( + libname='GUROBI', + name='GurobiOptimizer', + pip_install="pip install -i https://pypi.gurobi.com gurobipy") from ex # initialize model mdl = gp.Model(self.name) diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py index 7329b1786..9c1b705ea 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -18,7 +18,6 @@ from test.optimization_test_case import QiskitOptimizationTestCase from docplex.mp.model import DOcplexException, Model -import gurobipy as gp from qiskit.exceptions import MissingOptionalLibraryError from qiskit_optimization import INFINITY, QiskitOptimizationError, QuadraticProgram @@ -806,6 +805,11 @@ def test_docplex(self): def test_gurobipy(self): """test from_gurobipy and to_gurobipy""" + try: + import gurobipy as gp + except ImportError as ex: + self.skipTest("gurobipy not installed: {}".format(str(ex))) + return q_p = QuadraticProgram('test') q_p.binary_var(name='x') q_p.integer_var(name='y', lowerbound=-2, upperbound=4) From 0c7f0e59a5587d8de8889a1b17d91d05d6649a65 Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Sun, 21 Mar 2021 09:57:48 -0400 Subject: [PATCH 10/14] Add GurobiModel to typehints --- .../problems/quadratic_program.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 376cf0f1a..a868bbe33 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -36,6 +36,19 @@ from docplex.mp.quad import QuadExpr from docplex.mp.vartype import BinaryVarType, ContinuousVarType, IntegerVarType +try: + import gurobipy as gp + from gurobipy import Model as GurobiModel + _HAS_GUROBI = True +except ImportError: + _HAS_GUROBI = False + + class GurobiModel: # type: ignore + """ Empty GurobiModel class + Replacement if gurobipy.Model is not present. + """ + pass + from numpy import ndarray, zeros from scipy.sparse import spmatrix @@ -812,7 +825,7 @@ def maximize(self, self._objective = QuadraticObjective(self, constant, linear, quadratic, QuadraticObjective.Sense.MAXIMIZE) - def from_gurobipy(self, model: Any) -> None: + def from_gurobipy(self, model: GurobiModel) -> None: """Loads this quadratic program from a gurobipy model. Note that this supports only basic functions of gurobipy as follows: @@ -827,13 +840,11 @@ def from_gurobipy(self, model: Any) -> None: MissingOptionalLibraryError: gurobipy not installed QiskitOptimizationError: if the model contains unsupported elements. """ - try: - import gurobipy as gp - except ImportError as ex: + if not _HAS_GUROBI: raise MissingOptionalLibraryError( libname='GUROBI', name='GurobiOptimizer', - pip_install="pip install -i https://pypi.gurobi.com gurobipy") from ex + pip_install="pip install -i https://pypi.gurobi.com gurobipy") # clear current problem self.clear() @@ -1110,7 +1121,7 @@ def from_docplex(self, model: Model) -> None: raise QiskitOptimizationError( "Unsupported constraint sense: {}".format(constraint)) - def to_gurobipy(self) -> Any: + def to_gurobipy(self) -> GurobiModel: """Returns a gurobipy model corresponding to this quadratic program Returns: @@ -1120,13 +1131,11 @@ def to_gurobipy(self) -> Any: MissingOptionalLibraryError: gurobipy not installed QiskitOptimizationError: if non-supported elements (should never happen). """ - try: - import gurobipy as gp - except ImportError as ex: + if not _HAS_GUROBI: raise MissingOptionalLibraryError( libname='GUROBI', name='GurobiOptimizer', - pip_install="pip install -i https://pypi.gurobi.com gurobipy") from ex + pip_install="pip install -i https://pypi.gurobi.com gurobipy") # initialize model mdl = gp.Model(self.name) From 91cb275f2fd85eb845bd44255a6d1aef99f016f6 Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Sun, 21 Mar 2021 10:05:12 -0400 Subject: [PATCH 11/14] fix lint --- qiskit_optimization/problems/quadratic_program.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_optimization/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index a868bbe33..83d257c35 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -18,7 +18,7 @@ from collections.abc import Sequence from enum import Enum from math import fsum, isclose -from typing import Dict, List, Optional, Tuple, Union, cast, Any +from typing import Dict, List, Optional, Tuple, Union, cast import numpy as np from docplex.mp.constr import LinearConstraint as DocplexLinearConstraint From 57b1d521ff80ca17ec9b42db80c20cddc3502cf2 Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Wed, 24 Mar 2021 09:49:59 -0400 Subject: [PATCH 12/14] remove gurobipy from requirements.txt --- .github/actions/install-optimization/action.yml | 4 ++-- requirements.txt | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/actions/install-optimization/action.yml b/.github/actions/install-optimization/action.yml index b45d9a5d3..0ead7bccb 100644 --- a/.github/actions/install-optimization/action.yml +++ b/.github/actions/install-optimization/action.yml @@ -17,8 +17,8 @@ runs: using: "composite" steps: - run : | - # install requirements here because of gurobipy - pip install -U -r requirements.txt + # install gurobipy + pip install -i https://pypi.gurobi.com gurobipy pip install -e .[cplex,cvx] pip install -U -c constraints.txt -r requirements-dev.txt shell: bash \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 58e73f06c..3c7f538c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,3 @@ docplex==2.15.194; sys_platform == 'darwin' setuptools>=40.1.0 networkx>=2.2 dataclasses; python_version < '3.7' ---extra-index-url https://pypi.gurobi.com -gurobipy>=9.0.0 From 5b94fe23802c3fb446a8a3f78e051cddec7ccf9d Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Sun, 25 Apr 2021 12:37:55 -0400 Subject: [PATCH 13/14] Install gurobipy from public pypi --- .github/actions/install-optimization/action.yml | 2 -- requirements.txt | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/actions/install-optimization/action.yml b/.github/actions/install-optimization/action.yml index dfdfc05e3..21c6ff131 100644 --- a/.github/actions/install-optimization/action.yml +++ b/.github/actions/install-optimization/action.yml @@ -17,8 +17,6 @@ runs: using: "composite" steps: - run : | - # install gurobipy - pip install -i https://pypi.gurobi.com gurobipy pip install -e .[cplex,cvx,matplotlib] pip install -U -c constraints.txt -r requirements-dev.txt shell: bash diff --git a/requirements.txt b/requirements.txt index 3c7f538c9..7ac3283c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ docplex==2.15.194; sys_platform == 'darwin' setuptools>=40.1.0 networkx>=2.2 dataclasses; python_version < '3.7' +gurobipy From e3a539828a025431af3a22582911aa22ef974aa6 Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Mon, 26 Apr 2021 13:35:24 -0400 Subject: [PATCH 14/14] Use requires_extra_library decorator in unit test --- test/algorithms/test_gurobi_optimizer.py | 45 ++++++++++-------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/test/algorithms/test_gurobi_optimizer.py b/test/algorithms/test_gurobi_optimizer.py index c4eed172e..15ad11e12 100644 --- a/test/algorithms/test_gurobi_optimizer.py +++ b/test/algorithms/test_gurobi_optimizer.py @@ -13,9 +13,8 @@ """ Test Gurobi Optimizer """ import unittest -from test.optimization_test_case import QiskitOptimizationTestCase +from test.optimization_test_case import QiskitOptimizationTestCase, requires_extra_library from ddt import ddt, data -from qiskit.exceptions import MissingOptionalLibraryError from qiskit_optimization.algorithms import GurobiOptimizer from qiskit_optimization.problems import QuadraticProgram @@ -24,38 +23,30 @@ class TestGurobiOptimizer(QiskitOptimizationTestCase): """Gurobi Optimizer Tests.""" - def setUp(self): - super().setUp() - try: - self.gurobi_optimizer = GurobiOptimizer(disp=False) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) - @data( ('op_ip1.lp', [0, 2], 6), ('op_mip1.lp', [1, 1, 0], 6), ('op_lp1.lp', [0.25, 1.75], 5.8750) ) + @requires_extra_library def test_gurobi_optimizer(self, config): """ Gurobi Optimizer Test """ - try: - # unpack configuration - filename, x, fval = config - - # load optimization problem - problem = QuadraticProgram() - lp_file = self.get_resource_path(filename, 'algorithms/resources') - problem.read_from_lp_file(lp_file) - - # solve problem with gurobi - result = self.gurobi_optimizer.solve(problem) - - # analyze results - self.assertAlmostEqual(result.fval, fval) - for i in range(problem.get_num_vars()): - self.assertAlmostEqual(result.x[i], x[i]) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) + # unpack configuration + gurobi_optimizer = GurobiOptimizer(disp=False) + filename, x, fval = config + + # load optimization problem + problem = QuadraticProgram() + lp_file = self.get_resource_path(filename, 'algorithms/resources') + problem.read_from_lp_file(lp_file) + + # solve problem with gurobi + result = gurobi_optimizer.solve(problem) + + # analyze results + self.assertAlmostEqual(result.fval, fval) + for i in range(problem.get_num_vars()): + self.assertAlmostEqual(result.x[i], x[i]) if __name__ == '__main__':