diff --git a/.github/actions/install-optimization/action.yml b/.github/actions/install-optimization/action.yml index 21c6ff131..e03bbf60f 100644 --- a/.github/actions/install-optimization/action.yml +++ b/.github/actions/install-optimization/action.yml @@ -17,6 +17,6 @@ runs: using: "composite" steps: - run : | - pip install -e .[cplex,cvx,matplotlib] + pip install -e .[cplex,cvx,matplotlib,gurobi] pip install -U -c constraints.txt -r requirements-dev.txt shell: bash diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 852b3cd3f..ef4dd7f7e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -148,11 +148,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 c0d316d93..77f56f507 100644 --- a/.pylintdict +++ b/.pylintdict @@ -63,6 +63,9 @@ goldstone grover groveroptimizer gset +gurobi +gurobioptimizer +gurobipy gutmann hamilton hamiltonian @@ -100,6 +103,7 @@ minimumeigenoptimizer multiset ndarray ndarrays +noop networkx neven nosignatures @@ -159,6 +163,7 @@ str subcollection subgraph submodules +sys subproblem summands terra @@ -176,6 +181,8 @@ upperbound variational vartype vqe +writelines +xixj wavefunction wecker williamson 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/README.md b/README.md index 2401e6d9b..c1fcd5bc5 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,9 @@ To do this follow the instructions in the * **Matplotlib** may be installed using command `pip install 'qiskit-optimization[matplotlib]'` to install the package. Matplotlib being installed will enable the usage of the `draw` method in the graph optimization application classes. +* **Gurobipy** may be installed using command `pip install 'qiskit-optimization[gurobi]'` to install the + package. Gurobipy being installed will enable the usage of the GurobiOptimizer. + ### Creating Your First Optimization Programming Experiment in Qiskit Now that Qiskit Optimization is installed, it's time to begin working with the optimization module. diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 51f1f9b46..49417ea96 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -102,6 +102,9 @@ Optional installs * **Matplotlib**, may be installed using command `pip install 'qiskit-optimization[matplotlib]'` to install the package. Matplotlib being installed will enable the usage of the `draw` method in the graph optimization application classes. +* **Gurobipy** may be installed using command `pip install 'qiskit-optimization[gurobi]'` to install the + package. Gurobipy being installed will enable the usage of the GurobiOptimizer. + ---- Ready to get going?... 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..fa1b9fdea --- /dev/null +++ b/qiskit_optimization/algorithms/gurobi_optimizer.py @@ -0,0 +1,151 @@ +# 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 + +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) + """ + + 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/problems/quadratic_program.py b/qiskit_optimization/problems/quadratic_program.py index 6f92eec54..ca8f56978 100644 --- a/qiskit_optimization/problems/quadratic_program.py +++ b/qiskit_optimization/problems/quadratic_program.py @@ -35,6 +35,23 @@ from docplex.mp.model_reader import ModelReader 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 @@ -869,6 +886,145 @@ def maximize( self, constant, linear, quadratic, QuadraticObjective.Sense.MAXIMIZE ) + 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: + - quadratic objective function + - linear / quadratic constraints + - binary / integer / continuous variables + + Args: + model: The gurobipy model to be loaded. + + 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 -i https://pypi.gurobi.com gurobipy", + ) + + # 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 x.vtype == gp.GRB.CONTINUOUS: + x_new = self.continuous_var(x.lb, x.ub, x.VarName) + elif x.vtype == gp.GRB.BINARY: + x_new = self.binary_var(x.VarName) + 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.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: + self.minimize(constant, linear, quadratic) + 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(): + 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.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: + 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. @@ -1025,6 +1181,102 @@ def from_docplex(self, model: Model) -> None: else: raise QiskitOptimizationError("Unsupported constraint sense: {}".format(constraint)) + def to_gurobipy(self) -> GurobiModel: + """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). + """ + if not _HAS_GUROBI: + raise MissingOptionalLibraryError( + libname="GUROBI", + name="GurobiOptimizer", + pip_install="pip install -i https://pypi.gurobi.com gurobipy", + ) + + # 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/releasenotes/notes/add-gurobipy-94f45e32abb61c7b.yaml b/releasenotes/notes/add-gurobipy-94f45e32abb61c7b.yaml new file mode 100644 index 000000000..ec2ce3387 --- /dev/null +++ b/releasenotes/notes/add-gurobipy-94f45e32abb61c7b.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds :class`qiskit_optimization.algorithms.GurobiOptimizer`. + ``pip install qiskit_optimization[gurobi]`` installs + ``gurobipy`` to enable this optimizer. diff --git a/setup.py b/setup.py index cf4057c02..f9d90ea7d 100644 --- a/setup.py +++ b/setup.py @@ -64,6 +64,7 @@ 'cplex': ["cplex; python_version < '3.9'"], 'cvx': ['cvxpy'], 'matplotlib': ['matplotlib'], + 'gurobi': ['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..6a3e756f9 --- /dev/null +++ b/test/algorithms/test_gurobi_optimizer.py @@ -0,0 +1,51 @@ +# 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 03831d306..c64be23aa 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -966,6 +966,76 @@ 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""" + 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) + 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") + + # 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_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")