diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f961e9e81..446340e17 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -147,11 +147,11 @@ jobs: with: name: optimization${{ matrix.python-version }} path: ./o${{ matrix.python-version }}/* - - name: Optimization Unit Tests without cplex/cvxpy/matplotlib under Python ${{ matrix.python-version }} + - name: Optimization Unit Tests without cplex/cvxpy/matplotlib/gurobipy under Python ${{ matrix.python-version }} env: PYTHONWARNINGS: default run: | - pip uninstall -y cplex cvxpy matplotlib + pip uninstall -y cplex cvxpy matplotlib gurobipy if [ "${{ github.event_name }}" == "schedule" ] || [ "${{ contains(github.event.pull_request.labels.*.name, 'run_slow') }}" == "true" ]; then export QISKIT_TESTS="run_slow" fi diff --git a/.pylintdict b/.pylintdict index 0839ba96e..ead95c5a9 100644 --- a/.pylintdict +++ b/.pylintdict @@ -46,6 +46,8 @@ getter goemans grover gset +gurobi +gurobipy hamiltonian hamiltonians hardcoded @@ -79,6 +81,7 @@ minimumeigenoptimizationresult networkx ndarray ndarrays +noop nosignatures np num @@ -129,6 +132,7 @@ stdout str subgraph submodules +sys th toctree todo @@ -139,6 +143,7 @@ upperbound variational vartype vqe +writelines xixj xs xuxv diff --git a/.pylintrc b/.pylintrc index 2de22da43..a13ebe66b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -310,7 +310,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/algorithms/__init__.py b/qiskit_optimization/algorithms/__init__.py index 1705c65a7..fca1eb28a 100644 --- a/qiskit_optimization/algorithms/__init__.py +++ b/qiskit_optimization/algorithms/__init__.py @@ -47,6 +47,7 @@ GoemansWilliamsonOptimizationResult GroverOptimizationResult GroverOptimizer + GurobiOptimizer IntermediateResult MeanAggregator MinimumEigenOptimizationResult @@ -75,6 +76,7 @@ GoemansWilliamsonOptimizationResult, ) from .grover_optimizer import GroverOptimizer, GroverOptimizationResult +from .gurobi_optimizer import GurobiOptimizer from .minimum_eigen_optimizer import ( MinimumEigenOptimizer, MinimumEigenOptimizationResult, @@ -111,6 +113,7 @@ "GoemansWilliamsonOptimizationResult", "GroverOptimizer", "GroverOptimizationResult", + "GurobiOptimizer", "MeanAggregator", "MinimumEigenOptimizer", "MinimumEigenOptimizationResult", diff --git a/qiskit_optimization/algorithms/gurobi_optimizer.py b/qiskit_optimization/algorithms/gurobi_optimizer.py new file mode 100644 index 000000000..832cab8a3 --- /dev/null +++ b/qiskit_optimization/algorithms/gurobi_optimizer.py @@ -0,0 +1,152 @@ +# 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 Gurobi 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 +from ..translators.gurobipy import GurobipyTranslator + +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 + >>> # 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) if GurobiOptimizer.is_gurobi_installed() else None + """ + + 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_model(translator=GurobipyTranslator()) + + # 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/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 6c88b2f81..691a4f785 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -21,29 +21,14 @@ from typing import Dict, List, Optional, Tuple, Union, cast import numpy as np -from docplex.mp.constr import LinearConstraint as DocplexLinearConstraint -from docplex.mp.constr import NotEqualConstraint -from docplex.mp.constr import QuadraticConstraint as DocplexQuadraticConstraint - -try: - # new location since docplex 2.16.196 - from docplex.mp.dvar import Var -except ImportError: - # old location until docplex 2.15.194 - from docplex.mp.linear import Var from docplex.mp.model import Model from docplex.mp.model_reader import ModelReader -from docplex.mp.quad import QuadExpr -from docplex.mp.vartype import BinaryVarType, ContinuousVarType, IntegerVarType from numpy import ndarray, zeros -from scipy.sparse import spmatrix - from qiskit.exceptions import MissingOptionalLibraryError from qiskit.opflow import I, ListOp, OperatorBase, PauliOp, PauliSumOp, SummedOp from qiskit.quantum_info import Pauli +from scipy.sparse import spmatrix -from ..exceptions import QiskitOptimizationError -from ..infinity import INFINITY from .constraint import Constraint, ConstraintSense from .linear_constraint import LinearConstraint from .linear_expression import LinearExpression @@ -51,6 +36,9 @@ from .quadratic_expression import QuadraticExpression from .quadratic_objective import QuadraticObjective from .variable import Variable, VarType +from ..exceptions import QiskitOptimizationError +from ..infinity import INFINITY +from ..translators.model_translator import ModelTranslator logger = logging.getLogger(__name__) @@ -869,8 +857,8 @@ def maximize( self, constant, linear, quadratic, QuadraticObjective.Sense.MAXIMIZE ) - def from_docplex(self, model: Model) -> None: - """Loads this quadratic program from a docplex model. + def from_model(self, model: Model, translator: Optional[ModelTranslator] = None) -> None: + """Loads this quadratic program from an optimization model. Note that this supports only basic functions of docplex as follows: - quadratic objective function @@ -878,152 +866,51 @@ def from_docplex(self, model: Model) -> None: - binary / integer / continuous variables Args: - model: The docplex model to be loaded. + model: The optimization model to be loaded. + translator: The model translators Raises: QiskitOptimizationError: if the model contains unsupported elements. """ + if translator is None: + from qiskit_optimization.translators.docplex import DocplexTranslator - # clear current problem - self.clear() - - # get name - self.name = model.name - - # get variables - # keep track of names separately, since docplex allows to have None names. - var_names = {} - for x in model.iter_variables(): - if isinstance(x.vartype, ContinuousVarType): - x_new = self.continuous_var(x.lb, x.ub, x.name) - elif isinstance(x.vartype, BinaryVarType): - x_new = self.binary_var(x.name) - elif isinstance(x.vartype, IntegerVarType): - x_new = self.integer_var(x.lb, x.ub, x.name) - else: - raise QiskitOptimizationError( - "Unsupported variable type: {} {}".format(x.name, x.vartype) - ) - var_names[x] = x_new.name - - # objective sense - minimize = model.objective_sense.is_minimize() - - # make sure objective expression is linear or quadratic and not a variable - if isinstance(model.objective_expr, Var): - model.objective_expr = model.objective_expr + 0 - - # get objective offset - constant = model.objective_expr.constant - - # get linear part of objective - linear = {} - linear_part = model.objective_expr.get_linear_part() - for x in linear_part.iter_variables(): - linear[var_names[x]] = linear_part.get_coef(x) - - # get quadratic part of objective - quadratic = {} - if isinstance(model.objective_expr, QuadExpr): - for quad_triplet in model.objective_expr.iter_quad_triplets(): - i = var_names[quad_triplet[0]] - j = var_names[quad_triplet[1]] - v = quad_triplet[2] - quadratic[i, j] = v - - # set objective - if minimize: - self.minimize(constant, linear, quadratic) - else: - self.maximize(constant, linear, quadratic) - - # get linear constraints - for constraint in model.iter_constraints(): - if isinstance(constraint, DocplexQuadraticConstraint): - # ignore quadratic constraints here and process them later - continue - if not isinstance(constraint, DocplexLinearConstraint) or isinstance( - constraint, NotEqualConstraint - ): - # If any constraint is not linear/quadratic constraints, it raises an error. - # Notice that NotEqualConstraint is a subclass of Docplex's LinearConstraint, - # but it cannot be handled by optimization. - raise QiskitOptimizationError("Unsupported constraint: {}".format(constraint)) - name = constraint.name - sense = constraint.sense - - left_expr = constraint.get_left_expr() - right_expr = constraint.get_right_expr() - # for linear constraints we may get an instance of Var instead of expression, - # e.g. x + y = z - if isinstance(left_expr, Var): - left_expr = left_expr + 0 - if isinstance(right_expr, Var): - right_expr = right_expr + 0 - - rhs = right_expr.constant - left_expr.constant - - lhs = {} - for x in left_expr.iter_variables(): - lhs[var_names[x]] = left_expr.get_coef(x) - for x in right_expr.iter_variables(): - lhs[var_names[x]] = lhs.get(var_names[x], 0.0) - right_expr.get_coef(x) - - if sense == sense.EQ: - self.linear_constraint(lhs, "==", rhs, name) - elif sense == sense.GE: - self.linear_constraint(lhs, ">=", rhs, name) - elif sense == sense.LE: - self.linear_constraint(lhs, "<=", rhs, name) - else: - raise QiskitOptimizationError("Unsupported constraint sense: {}".format(constraint)) + translator = DocplexTranslator() + translator.model_to_qp(model, self) - # get quadratic constraints - for constraint in model.iter_quadratic_constraints(): - name = constraint.name - sense = constraint.sense + def to_model(self, translator: Optional[ModelTranslator] = None) -> Model: + """Returns an optimization model corresponding to this quadratic program. - left_expr = constraint.get_left_expr() - right_expr = constraint.get_right_expr() + Returns: + The optimization model corresponding to this quadratic program. - rhs = right_expr.constant - left_expr.constant - linear = {} - quadratic = {} + Raises: + QiskitOptimizationError: if non-supported elements (should never happen). + """ + if translator is None: + from qiskit_optimization.translators.docplex import DocplexTranslator - if left_expr.is_quad_expr(): - for x in left_expr.linear_part.iter_variables(): - linear[var_names[x]] = left_expr.linear_part.get_coef(x) - for quad_triplet in left_expr.iter_quad_triplets(): - i = var_names[quad_triplet[0]] - j = var_names[quad_triplet[1]] - v = quad_triplet[2] - quadratic[i, j] = v - else: - for x in left_expr.iter_variables(): - linear[var_names[x]] = left_expr.get_coef(x) - - if right_expr.is_quad_expr(): - for x in right_expr.linear_part.iter_variables(): - linear[var_names[x]] = linear.get( - var_names[x], 0.0 - ) - right_expr.linear_part.get_coef(x) - for quad_triplet in right_expr.iter_quad_triplets(): - i = var_names[quad_triplet[0]] - j = var_names[quad_triplet[1]] - v = quad_triplet[2] - quadratic[i, j] = quadratic.get((i, j), 0.0) - v - else: - for x in right_expr.iter_variables(): - linear[var_names[x]] = linear.get(var_names[x], 0.0) - right_expr.get_coef(x) - - if sense == sense.EQ: - self.quadratic_constraint(linear, quadratic, "==", rhs, name) - elif sense == sense.GE: - self.quadratic_constraint(linear, quadratic, ">=", rhs, name) - elif sense == sense.LE: - self.quadratic_constraint(linear, quadratic, "<=", rhs, name) - else: - raise QiskitOptimizationError("Unsupported constraint sense: {}".format(constraint)) + translator = DocplexTranslator() + return translator.qp_to_model(self) + + def from_docplex(self, model: Model) -> None: + """Loads this quadratic program from a docplex model. + + Note that this supports only basic functions of docplex as follows: + - quadratic objective function + - linear / quadratic constraints + - binary / integer / continuous variables + + Args: + model: The docplex model to be loaded. + + Raises: + QiskitOptimizationError: if the model contains unsupported elements. + """ + # warnings.warn("The from_docplex method is deprecated and will be " + # "removed in a future release. Instead use the" + # "from_model() method", DeprecationWarning) + self.from_model(model) def to_docplex(self) -> Model: """Returns a docplex model corresponding to this quadratic program. @@ -1034,81 +921,10 @@ def to_docplex(self) -> Model: Raises: QiskitOptimizationError: if non-supported elements (should never happen). """ - - # initialize model - mdl = Model(self.name) - - # add variables - var = {} - for idx, x in enumerate(self.variables): - if x.vartype == Variable.Type.CONTINUOUS: - var[idx] = mdl.continuous_var(lb=x.lowerbound, ub=x.upperbound, name=x.name) - elif x.vartype == Variable.Type.BINARY: - var[idx] = mdl.binary_var(name=x.name) - elif x.vartype == Variable.Type.INTEGER: - var[idx] = mdl.integer_var(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.minimize(objective) - else: - mdl.maximize(objective) - - # 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.add_constraint(linear_expr == rhs, ctname=name) - elif sense == Constraint.Sense.GE: - mdl.add_constraint(linear_expr >= rhs, ctname=name) - elif sense == Constraint.Sense.LE: - mdl.add_constraint(linear_expr <= rhs, ctname=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.add_constraint(quadratic_expr == rhs, ctname=name) - elif sense == Constraint.Sense.GE: - mdl.add_constraint(quadratic_expr >= rhs, ctname=name) - elif sense == Constraint.Sense.LE: - mdl.add_constraint(quadratic_expr <= rhs, ctname=name) - else: - # should never happen - raise QiskitOptimizationError("Unsupported constraint sense: {}".format(sense)) - - return mdl + # warnings.warn("The to_docplex method is deprecated and will be " + # "removed in a future release. Instead use the" + # "to_model() method", DeprecationWarning) + return self.to_model() def export_as_lp_string(self) -> str: """Returns the quadratic program as a string of LP format. @@ -1116,6 +932,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/qiskit_optimization/translators/__init__.py b/qiskit_optimization/translators/__init__.py new file mode 100644 index 000000000..e75a73c5c --- /dev/null +++ b/qiskit_optimization/translators/__init__.py @@ -0,0 +1,46 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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. + +""" +Optimization model translators (:mod:`qiskit_optimization.translators`) +=================================================================== + +.. currentmodule:: qiskit_optimization.translators + +Translators between an optimization model and a quadratic program + +Base classes for applications +======================================= + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + ModelTranslator + DocplexTranslator + GurobiTranslator + +Applications +====================== +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + DocplexTranslator + GurobiTranslator +""" + +from .model_translator import ModelTranslator +from .docplex import DocplexTranslator +from .gurobipy import GurobipyTranslator + +_all = ["ModelTranslator", "DocplexTranslator", "GurobipyTranslator"] diff --git a/qiskit_optimization/translators/docplex.py b/qiskit_optimization/translators/docplex.py new file mode 100644 index 000000000..cd6c1d4c7 --- /dev/null +++ b/qiskit_optimization/translators/docplex.py @@ -0,0 +1,288 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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. + +"""Model translators between QuadraticProgram and Docplex""" + +from typing import cast, Any + +from docplex.mp.constr import LinearConstraint as DocplexLinearConstraint +from docplex.mp.constr import NotEqualConstraint +from docplex.mp.constr import QuadraticConstraint as DocplexQuadraticConstraint +from docplex.mp.model import Model +from docplex.mp.quad import QuadExpr +from docplex.mp.vartype import BinaryVarType, ContinuousVarType, IntegerVarType + +try: + # new location since docplex 2.16.196 + from docplex.mp.dvar import Var +except ImportError: + # old location until docplex 2.15.194 + from docplex.mp.linear import Var + +from qiskit_optimization.exceptions import QiskitOptimizationError +from qiskit_optimization.problems.constraint import Constraint +from qiskit_optimization.problems.quadratic_objective import QuadraticObjective +from qiskit_optimization.problems.variable import Variable +from .model_translator import ModelTranslator + + +class DocplexTranslator(ModelTranslator): + """Translator between a Docplex model and a quadratic program""" + + def qp_to_model(self, quadratic_program: Any) -> Any: + """Returns a docplex model corresponding to a quadratic program. + + Args: + quadratic_program: The quadratic program to be translated + + Returns: + The docplex model corresponding to a quadratic program. + + Raises: + QiskitOptimizationError: if non-supported elements (should never happen). + """ + # from qiskit_optimization.problems.quadratic_program import QuadraticProgram + # quadratic_program = cast(QuadraticProgram, quadratic_program) + + # initialize model + mdl = Model(quadratic_program.name) + + # add variables + var = {} + for idx, x in enumerate(quadratic_program.variables): + if x.vartype == Variable.Type.CONTINUOUS: + var[idx] = mdl.continuous_var(lb=x.lowerbound, ub=x.upperbound, name=x.name) + elif x.vartype == Variable.Type.BINARY: + var[idx] = mdl.binary_var(name=x.name) + elif x.vartype == Variable.Type.INTEGER: + var[idx] = mdl.integer_var(lb=x.lowerbound, ub=x.upperbound, name=x.name) + else: + # should never happen + raise QiskitOptimizationError("Unsupported variable type: {}".format(x.vartype)) + + # add objective + objective = quadratic_program.objective.constant + for i, v in quadratic_program.objective.linear.to_dict().items(): + objective += v * var[cast(int, i)] + for (i, j), v in quadratic_program.objective.quadratic.to_dict().items(): + objective += v * var[cast(int, i)] * var[cast(int, j)] + if quadratic_program.objective.sense == QuadraticObjective.Sense.MINIMIZE: + mdl.minimize(objective) + else: + mdl.maximize(objective) + + # add linear constraints + for i, l_constraint in enumerate(quadratic_program.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.add_constraint(linear_expr == rhs, ctname=name) + elif sense == Constraint.Sense.GE: + mdl.add_constraint(linear_expr >= rhs, ctname=name) + elif sense == Constraint.Sense.LE: + mdl.add_constraint(linear_expr <= rhs, ctname=name) + else: + # should never happen + raise QiskitOptimizationError("Unsupported constraint sense: {}".format(sense)) + + # add quadratic constraints + for i, q_constraint in enumerate(quadratic_program.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.add_constraint(quadratic_expr == rhs, ctname=name) + elif sense == Constraint.Sense.GE: + mdl.add_constraint(quadratic_expr >= rhs, ctname=name) + elif sense == Constraint.Sense.LE: + mdl.add_constraint(quadratic_expr <= rhs, ctname=name) + else: + # should never happen + raise QiskitOptimizationError("Unsupported constraint sense: {}".format(sense)) + + return mdl + + def model_to_qp(self, model: Model, quadratic_program: Any): + """Translate a docplex model into a quadratic program. + + Note that this supports only basic functions of docplex as follows: + - quadratic objective function + - linear / quadratic constraints + - binary / integer / continuous variables + + Args: + model: The docplex model to be loaded. + quadratic_program: The quadratic program to be stored. + + Raises: + QiskitOptimizationError: if the model contains unsupported elements. + """ + # from qiskit_optimization.problems.quadratic_program import QuadraticProgram + # quadratic_program = cast(QuadraticProgram, quadratic_program) + + # clear current problem + quadratic_program.clear() + + # get name + quadratic_program.name = model.name + + # get variables + # keep track of names separately, since docplex allows to have None names. + var_names = {} + for x in model.iter_variables(): + if isinstance(x.vartype, ContinuousVarType): + x_new = quadratic_program.continuous_var(x.lb, x.ub, x.name) + elif isinstance(x.vartype, BinaryVarType): + x_new = quadratic_program.binary_var(x.name) + elif isinstance(x.vartype, IntegerVarType): + x_new = quadratic_program.integer_var(x.lb, x.ub, x.name) + else: + raise QiskitOptimizationError( + "Unsupported variable type: {} {}".format(x.name, x.vartype) + ) + var_names[x] = x_new.name + + # objective sense + minimize = model.objective_sense.is_minimize() + + # make sure objective expression is linear or quadratic and not a variable + if isinstance(model.objective_expr, Var): + model.objective_expr = model.objective_expr + 0 + + # get objective offset + constant = model.objective_expr.constant + + # get linear part of objective + linear = {} + linear_part = model.objective_expr.get_linear_part() + for x in linear_part.iter_variables(): + linear[var_names[x]] = linear_part.get_coef(x) + + # get quadratic part of objective + quadratic = {} + if isinstance(model.objective_expr, QuadExpr): + for quad_triplet in model.objective_expr.iter_quad_triplets(): + i = var_names[quad_triplet[0]] + j = var_names[quad_triplet[1]] + v = quad_triplet[2] + quadratic[i, j] = v + + # set objective + if minimize: + quadratic_program.minimize(constant, linear, quadratic) + else: + quadratic_program.maximize(constant, linear, quadratic) + + # get linear constraints + for constraint in model.iter_constraints(): + if isinstance(constraint, DocplexQuadraticConstraint): + # ignore quadratic constraints here and process them later + continue + if not isinstance(constraint, DocplexLinearConstraint) or isinstance( + constraint, NotEqualConstraint + ): + # If any constraint is not linear/quadratic constraints, it raises an error. + # Notice that NotEqualConstraint is a subclass of Docplex's LinearConstraint, + # but it cannot be handled by optimization. + raise QiskitOptimizationError("Unsupported constraint: {}".format(constraint)) + name = constraint.name + sense = constraint.sense + + left_expr = constraint.get_left_expr() + right_expr = constraint.get_right_expr() + # for linear constraints we may get an instance of Var instead of expression, + # e.g. x + y = z + if isinstance(left_expr, Var): + left_expr = left_expr + 0 + if isinstance(right_expr, Var): + right_expr = right_expr + 0 + + rhs = right_expr.constant - left_expr.constant + + lhs = {} + for x in left_expr.iter_variables(): + lhs[var_names[x]] = left_expr.get_coef(x) + for x in right_expr.iter_variables(): + lhs[var_names[x]] = lhs.get(var_names[x], 0.0) - right_expr.get_coef(x) + + if sense == sense.EQ: + quadratic_program.linear_constraint(lhs, "==", rhs, name) + elif sense == sense.GE: + quadratic_program.linear_constraint(lhs, ">=", rhs, name) + elif sense == sense.LE: + quadratic_program.linear_constraint(lhs, "<=", rhs, name) + else: + raise QiskitOptimizationError("Unsupported constraint sense: {}".format(constraint)) + + # get quadratic constraints + for constraint in model.iter_quadratic_constraints(): + name = constraint.name + sense = constraint.sense + + left_expr = constraint.get_left_expr() + right_expr = constraint.get_right_expr() + + rhs = right_expr.constant - left_expr.constant + linear = {} + quadratic = {} + + if left_expr.is_quad_expr(): + for x in left_expr.linear_part.iter_variables(): + linear[var_names[x]] = left_expr.linear_part.get_coef(x) + for quad_triplet in left_expr.iter_quad_triplets(): + i = var_names[quad_triplet[0]] + j = var_names[quad_triplet[1]] + v = quad_triplet[2] + quadratic[i, j] = v + else: + for x in left_expr.iter_variables(): + linear[var_names[x]] = left_expr.get_coef(x) + + if right_expr.is_quad_expr(): + for x in right_expr.linear_part.iter_variables(): + linear[var_names[x]] = linear.get( + var_names[x], 0.0 + ) - right_expr.linear_part.get_coef(x) + for quad_triplet in right_expr.iter_quad_triplets(): + i = var_names[quad_triplet[0]] + j = var_names[quad_triplet[1]] + v = quad_triplet[2] + quadratic[i, j] = quadratic.get((i, j), 0.0) - v + else: + for x in right_expr.iter_variables(): + linear[var_names[x]] = linear.get(var_names[x], 0.0) - right_expr.get_coef(x) + + if sense == sense.EQ: + quadratic_program.quadratic_constraint(linear, quadratic, "==", rhs, name) + elif sense == sense.GE: + quadratic_program.quadratic_constraint(linear, quadratic, ">=", rhs, name) + elif sense == sense.LE: + quadratic_program.quadratic_constraint(linear, quadratic, "<=", rhs, name) + else: + raise QiskitOptimizationError("Unsupported constraint sense: {}".format(constraint)) diff --git a/qiskit_optimization/translators/gurobipy.py b/qiskit_optimization/translators/gurobipy.py new file mode 100644 index 000000000..db50a4c8d --- /dev/null +++ b/qiskit_optimization/translators/gurobipy.py @@ -0,0 +1,289 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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. + +"""Model translators between QuadraticProgram and Gurobipy""" + +from typing import cast, Any + +from qiskit.exceptions import MissingOptionalLibraryError + +from qiskit_optimization.exceptions import QiskitOptimizationError +from qiskit_optimization.problems.constraint import Constraint +from qiskit_optimization.problems.quadratic_objective import QuadraticObjective +from qiskit_optimization.problems.variable import Variable +from .model_translator import ModelTranslator + +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 + + +class GurobipyTranslator(ModelTranslator): + """Translator between a gurobipy model and a quadratic program""" + + def qp_to_model(self, quadratic_program: Any) -> GurobiModel: + """Returns a gurobipy model corresponding to a quadratic program + + Returns: + The gurobipy model corresponding to a quadratic program. + + Raises: + MissingOptionalLibraryError: gurobipy not installed + QiskitOptimizationError: if non-supported elements (should never happen). + """ + if not _HAS_GUROBI: + raise MissingOptionalLibraryError( + libname="GUROBI", + name="GurobiOptimizer", + pip_install="pip install gurobipy", + ) + + from qiskit_optimization.problems.quadratic_program import QuadraticProgram + + quadratic_program = cast(QuadraticProgram, quadratic_program) + + # initialize model + mdl = gp.Model(quadratic_program.name) + + # add variables + var = {} + for idx, x in enumerate(quadratic_program.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 = quadratic_program.objective.constant + for i, v in quadratic_program.objective.linear.to_dict().items(): + objective += v * var[cast(int, i)] + for (i, j), v in quadratic_program.objective.quadratic.to_dict().items(): + objective += v * var[cast(int, i)] * var[cast(int, j)] + if quadratic_program.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(quadratic_program.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(quadratic_program.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 model_to_qp(self, model: GurobiModel, quadratic_program: Any): + """Translate a gurobipy model into a quadratic program. + + 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. + quadratic_program: The quadratic program to be stored. + + Raises: + MissingOptionalLibraryError: gurobipy not installed + QiskitOptimizationError: if the model contains unsupported elements. + """ + if not _HAS_GUROBI: + raise MissingOptionalLibraryError( + libname="GUROBI", + name="GurobiOptimizer", + pip_install="pip install gurobipy", + ) + + from qiskit_optimization.problems.quadratic_program import QuadraticProgram + + quadratic_program = cast(QuadraticProgram, quadratic_program) + + # clear current problem + quadratic_program.clear() + + # Update the model to make sure everything works as expected + model.update() + + # get name + quadratic_program.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 x.vtype == gp.GRB.CONTINUOUS: + x_new = quadratic_program.continuous_var(x.lb, x.ub, x.VarName) + elif x.vtype == gp.GRB.BINARY: + x_new = quadratic_program.binary_var(x.VarName) + elif x.vtype == gp.GRB.INTEGER: + x_new = quadratic_program.integer_var(x.lb, x.ub, x.VarName) + else: + raise QiskitOptimizationError( + "Unsupported variable type: {} {}".format(x.VarName, x.vtype) + ) + var_names[x] = x_new.name + + # 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: + quadratic_program.minimize(constant, linear, quadratic) + else: + quadratic_program.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(): + 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: + quadratic_program.linear_constraint(lhs, "==", rhs, name) + elif sense == gp.GRB.GREATER_EQUAL: + quadratic_program.linear_constraint(lhs, ">=", rhs, name) + elif sense == gp.GRB.LESS_EQUAL: + quadratic_program.linear_constraint(lhs, "<=", rhs, name) + else: + raise QiskitOptimizationError("Unsupported constraint sense: {}".format(constraint)) + + # get quadratic constraints + for constraint in model.getQConstrs(): + name = constraint.QCName + sense = constraint.QCSense + + 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: + quadratic_program.quadratic_constraint(linear, quadratic, "==", rhs, name) + elif sense == gp.GRB.GREATER_EQUAL: + quadratic_program.quadratic_constraint(linear, quadratic, ">=", rhs, name) + elif sense == gp.GRB.LESS_EQUAL: + quadratic_program.quadratic_constraint(linear, quadratic, "<=", rhs, name) + else: + raise QiskitOptimizationError("Unsupported constraint sense: {}".format(constraint)) diff --git a/qiskit_optimization/translators/model_translator.py b/qiskit_optimization/translators/model_translator.py new file mode 100644 index 000000000..a03f850e7 --- /dev/null +++ b/qiskit_optimization/translators/model_translator.py @@ -0,0 +1,42 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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. + +"""Abstract class for optimization model translators""" + +from abc import ABC, abstractmethod +from typing import Any + + +class ModelTranslator(ABC): + """Translator between an optimization model and a quadratic program""" + + @abstractmethod + def qp_to_model(self, quadratic_program: Any) -> Any: + """Returns an optimization model corresponding to a quadratic program. + + Args: + quadratic_program: The quadratic program to be translated + + Returns: + The optimization model corresponding to a quadratic program. + """ + pass + + @abstractmethod + def model_to_qp(self, model: Any, quadratic_program: Any) -> None: + """Translate an optimization model into a quadratic program. + + Args: + model: The optimization model to be loaded. + quadratic_program: The quadratic program to be stored. + """ + pass diff --git a/setup.py b/setup.py index cf4057c02..281659d57 100644 --- a/setup.py +++ b/setup.py @@ -64,6 +64,7 @@ 'cplex': ["cplex; python_version < '3.9'"], 'cvx': ['cvxpy'], 'matplotlib': ['matplotlib'], + 'gurobipy': ['gurobipy'], }, zip_safe=False ) diff --git a/test/algorithms/test_gurobi_optimizer.py b/test/algorithms/test_gurobi_optimizer.py new file mode 100644 index 000000000..cb707aff7 --- /dev/null +++ b/test/algorithms/test_gurobi_optimizer.py @@ -0,0 +1,56 @@ +# 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, + requires_extra_library, +) +from ddt import ddt, data +from qiskit_optimization.algorithms import GurobiOptimizer +from qiskit_optimization.problems import QuadraticProgram + + +@ddt +class TestGurobiOptimizer(QiskitOptimizationTestCase): + """Gurobi Optimizer Tests.""" + + @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""" + # 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__": + unittest.main() diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py index 6119c1d1e..7e750f16f 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -951,6 +951,83 @@ 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_model and to_model with GurobipyTranslator""" + try: + import gurobipy as gp + from qiskit_optimization.translators.gurobipy import GurobipyTranslator + + translator = GurobipyTranslator() + 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) + 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_model(q_p.to_model(translator=translator), translator=translator) + 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") + + # 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() + mod.addVar(vtype=gp.GRB.SEMIINT, lb=1, name="x") + q_p.from_model(mod, translator=translator) + + 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_model(mod, translator=translator) + + # 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_model(mod, translator=translator) + 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")