From 4e2ba0dc7f3f01d1f0dbacf8c807823bb95315f6 Mon Sep 17 00:00:00 2001 From: Stefan Woerner <41292468+stefan-woerner@users.noreply.github.com> Date: Mon, 27 Apr 2020 20:53:41 +0200 Subject: [PATCH] Qiskit Optimization (Qiskit/qiskit-aqua#877) * implement first part of steve's comments * skip some unnecessary varform checks * Revert "skip some unnecessary varform checks" * rename OptimizationProblem with QuadraticProgram * test cplex installation * fixed decode func for continuous variables * fixed format and added docstrings for the test * fix some codes after merge of upstream * change None for not impl error * is_compatible returns bool and raises * adding support for quadratic constraints * fixing imports * rename OptimizationProblem with QuadraticProgram in test_converters * commenting out quad constraints support * fix typos, add references * is_compatible for bool, get_incomptability for msg * incompatibility in admm / cplex optimizer * rename to compatibility_msg * == not > * update cplex optimizer * update cplex display settings * update recursive min optimizer docstring examples should be in class level, not module level fix class level docstring in general * (Re)move utils (Qiskit/qiskit-aqua#67) * reorganize utils * add missing init * fix import in test * (Re)move results (Qiskit/qiskit-aqua#68) * move results to algo files * remove solution/qualitymetrics for now they are safely stored on another branch but are currently not used so there is no reason to have them in this release * move the results objects to algorithms * update quadratic program, linear expression, linear constraint, variable and tests * add quadratic_expression * add quadratic constraint and tests * add quadratic objective * Update quadratic_program.py * add linear_constraint * revise constraints * fix tests and avoid cyclic import * fix default quadratic constraint name * Update test_quadratic_program.py * moving to a new optimization stack * update constraints handling * Update constraint.py * moving to a new optimization stack * add __getitem__() to linear and quadratic expressions * make constants upper case * (wip) substitute_variables * Alpha version of substitute_variables Also moved name arg of linear_constraint and quadratic_constraint in the last * moving to a new optimization stack * rename "coefficients_as_dict/array" to "to_dict/array" in linear/quadratic expressions * add read/write functionality to QP * Update quadratic_program.py * update cplex_optimizer and tests * Update quadratic_program.py * merge grover_optimizer updates * moving to a new optimization stack * cleanup * add a test of substitute_variables * replace use_index with use_name * allow integer as lower bound and upper bound of variable * change qp as well * update int to bin converter to use new QP interface * rename converters without "converter" in name * update qubo to operator + some minor fixes * moving to a new optimization stack * ignore trivial constraints (0 == 0) when converting to docplex * moving to a new optimization stack * Update quadratic_program.py * Update quadratic_program_to_qubo.py * moving to a new optimization stack * poc * modified InequalityToEquality converter for new QuadraticProgram APIs * Fix issues in integer_to_binary * bug fixes * removed importing cplex and fixied linting * update QP and tests * add __setitem__ to linear/quadratic expression * update recursive minimum eigen optimizer * removing 2* from quad objective * moving to a new optimization stack * removing 2* from quad objective * fix in step2 * fix in get_obj_val * fix in get_obj_val * fix in get_obj_val * Use compressed dictionary as the internal of quadratic expression * minor * Update optimization_algorithm.py * fix a type hint * moving to a new optimization stack * moving to a new optimization stack * use the internal Status directly * linting * rename penalize equality constraints converter * converter bug fixes * minor re-writing get_bet_merit_solution * minor re-writing get_bet_merit_solution * Fix unit tests related to quadratic expressions * Fix the declaration of __init__.py * Small fixes for pylint * moving to 'evaluate' in objective cost and constraints * added operator_to_quadratic_probram for new QP APIs * Current GroverOptimizer Code, New QP Interface WIP * GroverOptimizer Uses New QP Interface * added unittests * Add more tests of QuadraticProgram * Update test_quadratic_program_to_negative_value_oracle.py * formatting, renaming, minor changes * Update grover_optimizer.py * Add a missing sample LP file for test_quadratic_program * small fix of parsing problem name of LP file * fix a typo * only test based on exact solver (other redundant) * fix random seeds * add remove linear/quadratic constraints * remove util.py * Update quadratic_program_to_qubo.py * fix a factor * Bump Terra version requirements to 0.14.0 * fix var_form initialization * check cplex installed on unit tests * step2 generation based on a copy * step2 generation based on a copy * step2 generation based on a copy * formatting, renaming, minor changes * fixing converters / compatibility checks of algorithms * clean grover code (remove debugging lines) * fix IQFT and random seeds * Update test_quadratic_program_to_negative_value_oracle.py * Update operator_to_quadratic_program.py * Add QuadraticProgram.Status to replace SubstitutionStatus * add a test of status for clear * Embed enum data in classes * minor * tune * enable type hint of enums * supporting equality and quadratic constraints * more simplifications * admm tests on equality and quadratic constraints * fixes and more testing * deal with from_docplex corner cases * more simplifications, linting * Update quadratic_program.py use `model.iter_{linear, quadratic}_constraints()` instead of `model.get_constraint_by_index(i)`, since the latter calls CPLEX which is not available in Python 3.8. * easy part of steve's comments * part 2 of steve's comments * add comments * fix problem with int2bin converter * enabled int2bin converter * update problems __init__.py docstring * rename `HasQuadraticProgram` to `QuadraticProgramElement` * rename `HasQuadraticProgram` to `QuadraticProgramElement` (2) * module level docstrings, offset test * fix spell & test * leftover fixes * update changelog and ADMM docstring * fix write to dir, use new tempfile functionality * fix docstring * add changelog * remove Grover Optimization changelog * trigger CLA check * fix qasm string on unit tests Co-authored-by: Cryoris Co-authored-by: Manoel Marques Co-authored-by: Takashi Imamichi Co-authored-by: Anton Dekusar Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Co-authored-by: Atsushi Matsuo Co-authored-by: Austin Gilliam Co-authored-by: Julien Gacon Co-authored-by: Claudio Gambella Co-authored-by: adekusar-drl <62334182+adekusar-drl@users.noreply.github.com> --- qiskit/optimization/__init__.py | 23 +- qiskit/optimization/algorithms/__init__.py | 60 + .../optimization/algorithms/admm_optimizer.py | 723 +++++++++++ .../algorithms/cobyla_optimizer.py | 148 +++ .../algorithms/cplex_optimizer.py | 134 ++ .../algorithms/grover_optimizer.py | 404 ++++++ .../algorithms/minimum_eigen_optimizer.py | 247 ++++ .../algorithms/optimization_algorithm.py | 177 +++ .../recursive_minimum_eigen_optimizer.py | 210 ++++ qiskit/optimization/applications/__init__.py | 13 + .../{ => applications}/ising/__init__.py | 6 +- .../{ => applications}/ising/clique.py | 18 +- .../{ => applications}/ising/common.py | 8 +- .../{ => applications}/ising/docplex.py | 33 +- .../{ => applications}/ising/exact_cover.py | 6 +- .../ising/graph_partition.py | 2 +- .../{ => applications}/ising/knapsack.py | 0 .../{ => applications}/ising/max_cut.py | 0 .../{ => applications}/ising/partition.py | 2 +- .../{ => applications}/ising/set_packing.py | 8 +- .../{ => applications}/ising/stable_set.py | 8 +- .../{ => applications}/ising/tsp.py | 0 .../ising/vehicle_routing.py | 9 +- .../{ => applications}/ising/vertex_cover.py | 8 +- qiskit/optimization/converters/__init__.py | 58 + .../converters/inequality_to_equality.py | 359 ++++++ .../converters/integer_to_binary.py | 228 ++++ .../converters/linear_equality_to_penalty.py | 110 ++ .../operator_to_quadratic_program.py | 140 +++ ...dratic_program_to_negative_value_oracle.py | 178 +++ .../quadratic_program_to_operator.py | 119 ++ .../converters/quadratic_program_to_qubo.py | 128 ++ qiskit/optimization/exceptions.py | 22 + qiskit/optimization/infinity.py | 18 + qiskit/optimization/problems/__init__.py | 60 + qiskit/optimization/problems/constraint.py | 136 ++ .../problems/linear_constraint.py | 76 ++ .../problems/linear_expression.py | 156 +++ .../problems/quadratic_constraint.py | 105 ++ .../problems/quadratic_expression.py | 199 +++ .../problems/quadratic_objective.py | 153 +++ .../problems/quadratic_program.py | 1116 +++++++++++++++++ .../problems/quadratic_program_element.py | 45 + qiskit/optimization/problems/variable.py | 146 +++ test/optimization/__init__.py | 0 test/optimization/optimization_test_case.py | 0 test/optimization/resources/op_ip1.lp | 13 + test/optimization/resources/op_ip2.lp | 12 + test/optimization/resources/op_lp1.lp | 11 + test/optimization/resources/op_mip1.lp | 13 + .../resources/test_quadratic_program.lp | 25 + test/optimization/test_admm.py | 327 +++++ test/optimization/test_classical_cplex.py | 9 +- test/optimization/test_clique.py | 9 +- test/optimization/test_cobyla_optimizer.py | 69 + test/optimization/test_converters.py | 497 ++++++++ test/optimization/test_cplex_optimizer.py | 60 + test/optimization/test_docplex.py | 7 +- test/optimization/test_exact_cover.py | 9 +- test/optimization/test_graph_partition.py | 9 +- test/optimization/test_grover_optimizer.py | 104 ++ test/optimization/test_knapsack.py | 9 +- test/optimization/test_linear_constraint.py | 123 ++ test/optimization/test_linear_expression.py | 112 ++ test/optimization/test_min_eigen_optimizer.py | 91 ++ test/optimization/test_partition.py | 9 +- test/optimization/test_qaoa.py | 4 +- .../optimization/test_quadratic_constraint.py | 148 +++ .../optimization/test_quadratic_expression.py | 142 +++ test/optimization/test_quadratic_objective.py | 100 ++ test/optimization/test_quadratic_program.py | 643 ++++++++++ ...dratic_program_to_negative_value_oracle.py | 156 +++ test/optimization/test_readme_sample.py | 4 +- .../test_recursive_optimization.py | 68 + test/optimization/test_set_packing.py | 9 +- test/optimization/test_stable_set.py | 9 +- test/optimization/test_tsp.py | 9 +- test/optimization/test_variable.py | 58 + test/optimization/test_vehicle_routing.py | 7 +- test/optimization/test_vertex_cover.py | 11 +- 80 files changed, 8596 insertions(+), 89 deletions(-) create mode 100644 qiskit/optimization/algorithms/__init__.py create mode 100644 qiskit/optimization/algorithms/admm_optimizer.py create mode 100644 qiskit/optimization/algorithms/cobyla_optimizer.py create mode 100644 qiskit/optimization/algorithms/cplex_optimizer.py create mode 100644 qiskit/optimization/algorithms/grover_optimizer.py create mode 100644 qiskit/optimization/algorithms/minimum_eigen_optimizer.py create mode 100644 qiskit/optimization/algorithms/optimization_algorithm.py create mode 100644 qiskit/optimization/algorithms/recursive_minimum_eigen_optimizer.py create mode 100644 qiskit/optimization/applications/__init__.py rename qiskit/optimization/{ => applications}/ising/__init__.py (84%) rename qiskit/optimization/{ => applications}/ising/clique.py (91%) rename qiskit/optimization/{ => applications}/ising/common.py (97%) rename qiskit/optimization/{ => applications}/ising/docplex.py (91%) rename qiskit/optimization/{ => applications}/ising/exact_cover.py (97%) rename qiskit/optimization/{ => applications}/ising/graph_partition.py (99%) rename qiskit/optimization/{ => applications}/ising/knapsack.py (100%) rename qiskit/optimization/{ => applications}/ising/max_cut.py (100%) rename qiskit/optimization/{ => applications}/ising/partition.py (96%) rename qiskit/optimization/{ => applications}/ising/set_packing.py (92%) rename qiskit/optimization/{ => applications}/ising/stable_set.py (94%) rename qiskit/optimization/{ => applications}/ising/tsp.py (100%) rename qiskit/optimization/{ => applications}/ising/vehicle_routing.py (96%) rename qiskit/optimization/{ => applications}/ising/vertex_cover.py (92%) create mode 100644 qiskit/optimization/converters/__init__.py create mode 100644 qiskit/optimization/converters/inequality_to_equality.py create mode 100644 qiskit/optimization/converters/integer_to_binary.py create mode 100644 qiskit/optimization/converters/linear_equality_to_penalty.py create mode 100644 qiskit/optimization/converters/operator_to_quadratic_program.py create mode 100644 qiskit/optimization/converters/quadratic_program_to_negative_value_oracle.py create mode 100644 qiskit/optimization/converters/quadratic_program_to_operator.py create mode 100644 qiskit/optimization/converters/quadratic_program_to_qubo.py create mode 100644 qiskit/optimization/exceptions.py create mode 100644 qiskit/optimization/infinity.py create mode 100644 qiskit/optimization/problems/__init__.py create mode 100644 qiskit/optimization/problems/constraint.py create mode 100644 qiskit/optimization/problems/linear_constraint.py create mode 100644 qiskit/optimization/problems/linear_expression.py create mode 100644 qiskit/optimization/problems/quadratic_constraint.py create mode 100644 qiskit/optimization/problems/quadratic_expression.py create mode 100644 qiskit/optimization/problems/quadratic_objective.py create mode 100644 qiskit/optimization/problems/quadratic_program.py create mode 100644 qiskit/optimization/problems/quadratic_program_element.py create mode 100644 qiskit/optimization/problems/variable.py mode change 100644 => 100755 test/optimization/__init__.py mode change 100644 => 100755 test/optimization/optimization_test_case.py create mode 100644 test/optimization/resources/op_ip1.lp create mode 100644 test/optimization/resources/op_ip2.lp create mode 100644 test/optimization/resources/op_lp1.lp create mode 100644 test/optimization/resources/op_mip1.lp create mode 100644 test/optimization/resources/test_quadratic_program.lp create mode 100644 test/optimization/test_admm.py mode change 100644 => 100755 test/optimization/test_classical_cplex.py mode change 100644 => 100755 test/optimization/test_clique.py create mode 100644 test/optimization/test_cobyla_optimizer.py create mode 100644 test/optimization/test_converters.py create mode 100644 test/optimization/test_cplex_optimizer.py mode change 100644 => 100755 test/optimization/test_docplex.py mode change 100644 => 100755 test/optimization/test_exact_cover.py mode change 100644 => 100755 test/optimization/test_graph_partition.py create mode 100644 test/optimization/test_grover_optimizer.py create mode 100644 test/optimization/test_linear_constraint.py create mode 100644 test/optimization/test_linear_expression.py create mode 100644 test/optimization/test_min_eigen_optimizer.py mode change 100644 => 100755 test/optimization/test_partition.py mode change 100644 => 100755 test/optimization/test_qaoa.py create mode 100644 test/optimization/test_quadratic_constraint.py create mode 100644 test/optimization/test_quadratic_expression.py create mode 100644 test/optimization/test_quadratic_objective.py create mode 100644 test/optimization/test_quadratic_program.py create mode 100644 test/optimization/test_quadratic_program_to_negative_value_oracle.py create mode 100755 test/optimization/test_recursive_optimization.py mode change 100644 => 100755 test/optimization/test_set_packing.py create mode 100644 test/optimization/test_variable.py mode change 100644 => 100755 test/optimization/test_vehicle_routing.py mode change 100644 => 100755 test/optimization/test_vertex_cover.py diff --git a/qiskit/optimization/__init__.py b/qiskit/optimization/__init__.py index f94b1c5d3..e302d9592 100644 --- a/qiskit/optimization/__init__.py +++ b/qiskit/optimization/__init__.py @@ -15,22 +15,31 @@ ==================================================================== Optimization application stack for Aqua (:mod:`qiskit.optimization`) ==================================================================== -This is the finance domain logic.... .. currentmodule:: qiskit.optimization -Submodules -========== +Contents +======== .. autosummary:: - :toctree: + :toctree: ../stubs/ + :nosignatures: - ising + QuadraticProgram + QiskitOptimizationError + INFINITY """ +from .infinity import INFINITY # must be at the top of the file +from .exceptions import QiskitOptimizationError +from .problems import QuadraticProgram from ._logging import (get_qiskit_optimization_logging, set_qiskit_optimization_logging) -__all__ = ['get_qiskit_optimization_logging', - 'set_qiskit_optimization_logging'] +__all__ = ['QuadraticProgram', + 'QiskitOptimizationError', + 'get_qiskit_optimization_logging', + 'set_qiskit_optimization_logging', + 'INFINITY' + ] diff --git a/qiskit/optimization/algorithms/__init__.py b/qiskit/optimization/algorithms/__init__.py new file mode 100644 index 000000000..4ede3a37c --- /dev/null +++ b/qiskit/optimization/algorithms/__init__.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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 stack for Aqua (:mod:`qiskit.optimization.algorithms`) +=================================================================== + +Algorithms for optimization algorithms. + +.. currentmodule:: qiskit.optimization.algorithms + +Base class +========== + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + OptimizationAlgorithm + +Algorithms +========== + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + ADMMOptimizer + CobylaOptimizer + CplexOptimizer + GroverOptimizer + MinimumEigenOptimizer + RecursiveMinimumEigenOptimizer + +""" + +from .optimization_algorithm import OptimizationResult +from .optimization_algorithm import OptimizationAlgorithm +from .admm_optimizer import ADMMOptimizer +from .cplex_optimizer import CplexOptimizer +from .cobyla_optimizer import CobylaOptimizer +from .minimum_eigen_optimizer import MinimumEigenOptimizer +from .recursive_minimum_eigen_optimizer import RecursiveMinimumEigenOptimizer +from .grover_optimizer import GroverOptimizer, GroverOptimizationResults + +__all__ = ["ADMMOptimizer", "OptimizationAlgorithm", "OptimizationResult", "CplexOptimizer", + "CobylaOptimizer", "MinimumEigenOptimizer", "RecursiveMinimumEigenOptimizer", + "GroverOptimizer", "GroverOptimizationResults"] diff --git a/qiskit/optimization/algorithms/admm_optimizer.py b/qiskit/optimization/algorithms/admm_optimizer.py new file mode 100644 index 000000000..bbb20897f --- /dev/null +++ b/qiskit/optimization/algorithms/admm_optimizer.py @@ -0,0 +1,723 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""An implementation of the ADMM algorithm.""" +import copy +import logging +import time +from typing import List, Optional, Any + +import numpy as np +from qiskit.optimization import QiskitOptimizationError +from qiskit.optimization.algorithms.cplex_optimizer import CplexOptimizer +from qiskit.optimization.algorithms.optimization_algorithm import (OptimizationAlgorithm, + OptimizationResult) +from qiskit.optimization.converters import IntegerToBinary +from qiskit.optimization.problems import QuadraticProgram, Variable, Constraint, QuadraticObjective + +UPDATE_RHO_BY_TEN_PERCENT = 0 +UPDATE_RHO_BY_RESIDUALS = 1 + +logger = logging.getLogger(__name__) + + +class ADMMParameters: + """Defines a set of parameters for ADMM optimizer.""" + + def __init__(self, rho_initial: float = 10000, factor_c: float = 100000, beta: float = 1000, + max_iter: int = 10, tol: float = 1.e-4, max_time: float = np.inf, + three_block: bool = True, vary_rho: int = UPDATE_RHO_BY_TEN_PERCENT, + tau_incr: float = 2, tau_decr: float = 2, mu_res: float = 10, + mu_merit: float = 1000) -> None: + """Defines parameters for ADMM optimizer and their default values. + + Args: + rho_initial: Initial value of rho parameter of ADMM. + factor_c: Penalizing factor for equality constraints, when mapping to QUBO. + beta: Penalization for y decision variables. + max_iter: Maximum number of iterations for ADMM. + tol: Tolerance for the residual convergence. + max_time: Maximum running time (in seconds) for ADMM. + three_block: Boolean flag to select the 3-block ADMM implementation. + vary_rho: Flag to select the rule to update rho. + If set to 0, then rho increases by 10% at each iteration. + If set to 1, then rho is modified according to primal and dual residuals. + tau_incr: Parameter used in the rho update (UPDATE_RHO_BY_RESIDUALS). + The update rule can be found in: + Boyd, S., Parikh, N., Chu, E., Peleato, B., & Eckstein, J. (2011). + Distributed optimization and statistical learning via the alternating + direction method of multipliers. + Foundations and TrendsĀ® in Machine learning, 3(1), 1-122. + tau_decr: Parameter used in the rho update (UPDATE_RHO_BY_RESIDUALS). + mu_res: Parameter used in the rho update (UPDATE_RHO_BY_RESIDUALS). + mu_merit: Penalization for constraint residual. Used to compute the merit values. + """ + super().__init__() + self.mu_merit = mu_merit + self.mu_res = mu_res + self.tau_decr = tau_decr + self.tau_incr = tau_incr + self.vary_rho = vary_rho + self.three_block = three_block + self.max_time = max_time + self.tol = tol + self.max_iter = max_iter + self.factor_c = factor_c + self.beta = beta + self.rho_initial = rho_initial + + +class ADMMState: + """Internal computation state of the ADMM implementation. + + The state keeps track of various variables are stored that are being updated during problem + solving. The values are relevant to the problem being solved. The state is recreated for each + optimization problem. State is returned as the third value. + """ + + def __init__(self, + op: QuadraticProgram, + binary_indices: List[int], + continuous_indices: List[int], + rho_initial: float) -> None: + """ + Args: + op: The optimization problem being solved. + binary_indices: Indices of the binary decision variables of the original problem. + continuous_indices: Indices of the continuous decision variables of the original + problem. + rho_initial: Initial value of the rho parameter. + """ + super().__init__() + + # Optimization problem itself + self.op = op + # Indices of the variables + self.binary_indices = binary_indices + self.continuous_indices = continuous_indices + + # define heavily used matrix, they are used at each iteration, so let's cache them, + # they are np.ndarrays + # pylint:disable=invalid-name + # objective + self.q0 = None + self.c0 = None + self.q1 = None + self.c1 = None + # constraints + self.a0 = None + self.b0 = None + + # These are the parameters that are updated in the ADMM iterations. + self.u: np.ndarray = np.zeros(len(continuous_indices)) + binary_size = len(binary_indices) + self.x0: np.ndarray = np.zeros(binary_size) + self.z: np.ndarray = np.zeros(binary_size) + self.z_init: np.ndarray = self.z + self.y: np.ndarray = np.zeros(binary_size) + self.lambda_mult: np.ndarray = np.zeros(binary_size) + + # The following structures store quantities obtained in each ADMM iteration. + self.cost_iterates = [] + self.residuals = [] + self.dual_residuals = [] + self.cons_r = [] + self.merits = [] + self.lambdas = [] + self.x0_saved = [] + self.u_saved = [] + self.z_saved = [] + self.y_saved = [] + self.rho = rho_initial + + self.binary_equality_constraints = [] # lin. eq. constraints with bin. vars. only + self.equality_constraints = [] # all equality constraints + self.inequality_constraints = [] # all inequality constraints + + +class ADMMOptimizationResult(OptimizationResult): + """ ADMMOptimization Result.""" + + def __init__(self, x: Optional[Any] = None, fval: Optional[Any] = None, + state: Optional[ADMMState] = None, results: Optional[Any] = None) -> None: + super().__init__(x, fval, results or state) + self._state = state + + @property + def state(self) -> Optional[ADMMState]: + """ returns state """ + return self._state + + +class ADMMOptimizer(OptimizationAlgorithm): + """An implementation of the ADMM-based heuristic. + + This algorithm is introduced in [1]. + + **References:** + + [1] Gambella, C., & Simonetto, A. (2020). Multi-block ADMM Heuristics for Mixed-Binary + Optimization on Classical and Quantum Computers. arXiv preprint arXiv:2001.02069. + """ + + def __init__(self, qubo_optimizer: Optional[OptimizationAlgorithm] = None, + continuous_optimizer: Optional[OptimizationAlgorithm] = None, + params: Optional[ADMMParameters] = None) -> None: + """ + Args: + qubo_optimizer: An instance of OptimizationAlgorithm that can effectively solve + QUBO problems. + continuous_optimizer: An instance of OptimizationAlgorithm that can solve + continuous problems. + params: An instance of ADMMParameters. + + Raises: + NameError: CPLEX is not installed. + """ + super().__init__() + self._log = logging.getLogger(__name__) + + # create default params if not present + self._params = params or ADMMParameters() + + # create optimizers if not specified + self._qubo_optimizer = qubo_optimizer or CplexOptimizer() + self._continuous_optimizer = continuous_optimizer or CplexOptimizer() + + # internal state where we'll keep intermediate solution + # here, we just declare the class variable, the variable is initialized in kept in + # the solve method. + self._state: Optional[ADMMState] = None + + def get_compatibility_msg(self, problem: QuadraticProgram) -> Optional[str]: + """Checks whether a given problem can be solved with the optimizer implementing this method. + + Args: + problem: The optimization problem to check compatibility. + + Returns: + Returns True if the problem is compatible, otherwise raises an error. + + Raises: + QiskitOptimizationError: If the problem is not compatible with the ADMM optimizer. + """ + + msg = '' + + # 1. get bin/int and continuous variable indices + bin_int_indices = self._get_variable_indices(problem, Variable.Type.BINARY) + continuous_indices = self._get_variable_indices(problem, Variable.Type.CONTINUOUS) + + # 2. binary and continuous variables are separable in objective + for bin_int_index in bin_int_indices: + for continuous_index in continuous_indices: + coeff = problem.objective.quadratic[bin_int_index, continuous_index] + if coeff != 0: + # binary and continuous vars are mixed. + msg += 'Binary and continuous variables are not separable in the objective. ' + + # if an error occurred, return error message, otherwise, return None + return msg + + def solve(self, problem: QuadraticProgram) -> ADMMOptimizationResult: + """Tries to solves the given problem using ADMM algorithm. + + Args: + problem: The problem to be solved. + + Returns: + The result of the optimizer applied to the problem. + + Raises: + QiskitOptimizationError: If the problem is not compatible with the ADMM optimizer. + """ + # check compatibility and raise exception if incompatible + msg = self.get_compatibility_msg(problem) + if len(msg) > 0: + raise QiskitOptimizationError('Incompatible problem: {}'.format(msg)) + + # debug + self._log.debug("Initial problem: %s", problem.export_as_lp_string()) + + # map integer variables to binary variables + int2bin = IntegerToBinary() + problem = int2bin.encode(problem) + + # we deal with minimization in the optimizer, so turn the problem to minimization + problem, sense = self._turn_to_minimization(problem) + + # parse problem and convert to an ADMM specific representation. + binary_indices = self._get_variable_indices(problem, Variable.Type.BINARY) + continuous_indices = self._get_variable_indices(problem, Variable.Type.CONTINUOUS) + + # create our computation state. + self._state = ADMMState(problem, binary_indices, + continuous_indices, self._params.rho_initial) + + # convert optimization problem to a set of matrices and vector that are used + # at each iteration. + self._convert_problem_representation() + + start_time = time.time() + # we have not stated our computations yet, so elapsed time initialized as zero. + elapsed_time = 0 + iteration = 0 + residual = 1.e+2 + + while (iteration < self._params.max_iter and residual > self._params.tol) \ + and (elapsed_time < self._params.max_time): + if binary_indices: + op1 = self._create_step1_problem() + self._state.x0 = self._update_x0(op1) + # debug + self._log.debug("Step 1 sub-problem: %s", op1.export_as_lp_string()) + # else, no binary variables exist, and no update to be done in this case. + # debug + self._log.debug("x0=%s", self._state.x0) + + op2 = self._create_step2_problem() + self._state.u, self._state.z = self._update_x1(op2) + # debug + self._log.debug("Step 2 sub-problem: %s", op2.export_as_lp_string()) + self._log.debug("u=%s", self._state.u) + self._log.debug("z=%s", self._state.z) + + if self._params.three_block: + if binary_indices: + op3 = self._create_step3_problem() + self._state.y = self._update_y(op3) + # debug + self._log.debug("Step 3 sub-problem: %s", op3.export_as_lp_string()) + # debug + self._log.debug("y=%s", self._state.y) + + self._state.lambda_mult = self._update_lambda_mult() + # debug + self._log.debug("lambda: %s", self._state.lambda_mult) + + cost_iterate = self._get_objective_value() + constraint_residual = self._get_constraint_residual() + residual, dual_residual = self._get_solution_residuals(iteration) + merit = self._get_merit(cost_iterate, constraint_residual) + # debug + self._log.debug("cost_iterate=%s, cr=%s, merit=%s", + cost_iterate, constraint_residual, merit) + + # costs are saved with their original sign. + self._state.cost_iterates.append(cost_iterate) + self._state.residuals.append(residual) + self._state.dual_residuals.append(dual_residual) + self._state.cons_r.append(constraint_residual) + self._state.merits.append(merit) + self._state.lambdas.append(np.linalg.norm(self._state.lambda_mult)) + + self._state.x0_saved.append(self._state.x0) + self._state.u_saved.append(self._state.u) + self._state.z_saved.append(self._state.z) + self._state.z_saved.append(self._state.y) + + self._update_rho(residual, dual_residual) + + iteration += 1 + elapsed_time = time.time() - start_time + + binary_vars, continuous_vars, objective_value = self._get_best_merit_solution() + solution = self._revert_solution_indexes(binary_vars, continuous_vars) + + # flip the objective sign again if required + objective_value = objective_value * sense + + # third parameter is our internal state of computations. + result = ADMMOptimizationResult(solution, objective_value, self._state) + + # convert back integer to binary + result = int2bin.decode(result) + # debug + self._log.debug("solution=%s, objective=%s at iteration=%s", + solution, objective_value, iteration) + return result + + @staticmethod + def _turn_to_minimization(problem: QuadraticProgram) -> (QuadraticProgram, float): + """ + Turns the problem to `ObjSense.MINIMIZE` by flipping the sign of the objective function + if initially it is `ObjSense.MAXIMIZE`. Otherwise returns the original problem. + + Args: + problem: a problem to turn to minimization. + + Returns: + A copy of the problem if sign flip is required, otherwise the original problem and + the original sense of the problem in the numerical representation. + """ + sense = problem.objective.sense.value + if problem.objective.sense == QuadraticObjective.Sense.MAXIMIZE: + problem = copy.deepcopy(problem) + problem.objective.sense = QuadraticObjective.Sense.MINIMIZE + problem.objective.constant = (-1) * problem.objective.constant + problem.objective.linear = (-1) * problem.objective.linear.coefficients + problem.objective.quadratic = (-1) * problem.objective.quadratic.coefficients + return problem, sense + + @staticmethod + def _get_variable_indices(op: QuadraticProgram, var_type: Variable.Type) -> List[int]: + """Returns a list of indices of the variables of the specified type. + + Args: + op: Optimization problem. + var_type: type of variables to look for. + + Returns: + List of indices. + """ + indices = [] + for i, variable in enumerate(op.variables): + if variable.vartype == var_type: + indices.append(i) + + return indices + + def _get_current_solution(self) -> np.ndarray: + """ + Returns current solution of the problem. + + Returns: + An array of the current solution. + """ + return self._revert_solution_indexes(self._state.x0, self._state.u) + + def _revert_solution_indexes(self, binary_vars: np.ndarray, continuous_vars: np.ndarray) \ + -> np.ndarray: + """Constructs a solution array where variables are stored in the correct order. + + Args: + binary_vars: solution for binary variables + continuous_vars: solution for continuous variables + + Returns: + A solution array. + """ + solution = np.zeros(len(self._state.binary_indices) + len(self._state.continuous_indices)) + # restore solution at the original index location + solution.put(self._state.binary_indices, binary_vars) + solution.put(self._state.continuous_indices, continuous_vars) + return solution + + def _convert_problem_representation(self) -> None: + """Converts problem representation into set of matrices and vectors.""" + binary_var_indices = set(self._state.binary_indices) + # separate constraints + for constraint in self._state.op.linear_constraints: + if constraint.sense == Constraint.Sense.EQ: + self._state.equality_constraints.append(constraint) + + # verify that there are only binary variables in the constraint + # this is to build A0, b0 in step 1 + constraint_var_indices = set(constraint.linear.to_dict().keys()) + if constraint_var_indices.issubset(binary_var_indices): + self._state.binary_equality_constraints.append(constraint) + + elif constraint.sense in (Constraint.Sense.LE, Constraint.Sense.GE): + self._state.inequality_constraints.append(constraint) + + # separate quadratic constraints into eq and non-eq + for constraint in self._state.op.quadratic_constraints: + if constraint.sense == Constraint.Sense.EQ: + self._state.equality_constraints.append(constraint) + elif constraint.sense in (Constraint.Sense.LE, Constraint.Sense.GE): + self._state.inequality_constraints.append(constraint) + + # objective + self._state.q0 = self._get_q(self._state.binary_indices) + self._state.c0 = self._state.op.objective.linear.to_array()[self._state.binary_indices] + self._state.q1 = self._get_q(self._state.continuous_indices) + self._state.c1 = self._state.op.objective.linear.to_array()[self._state.continuous_indices] + # equality constraints with binary vars only + self._state.a0, self._state.b0 = self._get_a0_b0() + + def _get_q(self, variable_indices: List[int]) -> np.ndarray: + """Constructs a quadratic matrix for the variables with the specified indices + from the quadratic terms in the objective. + + Args: + variable_indices: variable indices to look for. + + Returns: + A matrix as a numpy array of the shape(len(variable_indices), len(variable_indices)). + """ + size = len(variable_indices) + q = np.zeros(shape=(size, size)) + # fill in the matrix + # in fact we use re-indexed variables + for i, var_index_i in enumerate(variable_indices): + for j, var_index_j in enumerate(variable_indices): + # coefficients_as_array + q[i, j] = self._state.op.objective.quadratic[var_index_i, var_index_j] + + return q + + def _get_a0_b0(self) -> (np.ndarray, np.ndarray): + """Constructs a matrix and a vector from the constraints in a form of Ax = b, where + x is a vector of binary variables. + + Returns: + Corresponding matrix and vector as numpy arrays. + + Raises: + ValueError: if the problem is not suitable for this optimizer. + """ + matrix = [] + vector = [] + + for constraint in self._state.binary_equality_constraints: + row = constraint.linear.to_array().take(self._state.binary_indices).tolist() + + matrix.append(row) + vector.append(constraint.rhs) + + if len(matrix) != 0: + np_matrix = np.array(matrix) + np_vector = np.array(vector) + else: + np_matrix = np.array([0] * len(self._state.binary_indices)).reshape((1, -1)) + np_vector = np.zeros(shape=(1,)) + + return np_matrix, np_vector + + def _create_step1_problem(self) -> QuadraticProgram: + """Creates a step 1 sub-problem. + + Returns: + A newly created optimization problem. + """ + op1 = QuadraticProgram() + + binary_size = len(self._state.binary_indices) + # create the same binary variables. + for i in range(binary_size): + op1.binary_var(name="x0_" + str(i + 1)) + + # prepare and set quadratic objective. + quadratic_objective = self._state.q0 +\ + self._params.factor_c / 2 * np.dot(self._state.a0.transpose(), self._state.a0) +\ + self._state.rho / 2 * np.eye(binary_size) + op1.objective.quadratic = quadratic_objective + + # prepare and set linear objective. + linear_objective = self._state.c0 - \ + self._params.factor_c * np.dot(self._state.b0, self._state.a0) + \ + self._state.rho * (- self._state.y - self._state.z) + \ + self._state.lambda_mult + + op1.objective.linear = linear_objective + return op1 + + def _create_step2_problem(self) -> QuadraticProgram: + """Creates a step 2 sub-problem. + + Returns: + A newly created optimization problem. + """ + op2 = copy.deepcopy(self._state.op) + # replace binary variables with the continuous ones bound in [0,1] + # x0(bin) -> z(cts) + # u (cts) are still there unchanged + for i, var_index in enumerate(self._state.binary_indices): + variable = op2.variables[var_index] + variable.vartype = Variable.Type.CONTINUOUS + variable.upperbound = 1. + variable.lowerbound = 0. + # replacing Q0 objective and take of min/max sense, initially we consider minimization + op2.objective.quadratic[var_index, var_index] = self._state.rho / 2 + # replacing linear objective + op2.objective.linear[var_index] = -1 * self._state.lambda_mult[i] - self._state.rho * \ + (self._state.x0[i] - self._state.y[i]) + + # remove A0 x0 = b0 constraints + for constraint in self._state.binary_equality_constraints: + op2.remove_linear_constraint(constraint.name) + + return op2 + + def _create_step3_problem(self) -> QuadraticProgram: + """Creates a step 3 sub-problem. + + Returns: + A newly created optimization problem. + """ + op3 = QuadraticProgram() + # add y variables. + binary_size = len(self._state.binary_indices) + for i in range(binary_size): + op3.continuous_var(name="y_" + str(i + 1), lowerbound=-np.inf, upperbound=np.inf) + + # set quadratic objective y + quadratic_y = self._params.beta / 2 * np.eye(binary_size) + \ + self._state.rho / 2 * np.eye(binary_size) + op3.objective.quadratic = quadratic_y + + # set linear objective for y + linear_y = - self._state.lambda_mult - self._state.rho * (self._state.x0 - self._state.z) + op3.objective.linear = linear_y + + return op3 + + def _update_x0(self, op1: QuadraticProgram) -> np.ndarray: + """Solves the Step1 QuadraticProgram via the qubo optimizer. + + Args: + op1: the Step1 QuadraticProgram. + + Returns: + A solution of the Step1, as a numpy array. + """ + return np.asarray(self._qubo_optimizer.solve(op1).x) + + def _update_x1(self, op2: QuadraticProgram) -> (np.ndarray, np.ndarray): + """Solves the Step2 QuadraticProgram via the continuous optimizer. + + Args: + op2: the Step2 QuadraticProgram + + Returns: + A solution of the Step2, as a pair of numpy arrays. + First array contains the values of decision variables u, and + second array contains the values of decision variables z. + + """ + vars_op2 = np.asarray(self._continuous_optimizer.solve(op2).x) + vars_u = vars_op2.take(self._state.continuous_indices) + vars_z = vars_op2.take(self._state.binary_indices) + return vars_u, vars_z + + def _update_y(self, op3: QuadraticProgram) -> np.ndarray: + """Solves the Step3 QuadraticProgram via the continuous optimizer. + + Args: + op3: the Step3 QuadraticProgram + + Returns: + A solution of the Step3, as a numpy array. + + """ + return np.asarray(self._continuous_optimizer.solve(op3).x) + + def _get_best_merit_solution(self) -> (np.ndarray, np.ndarray, float): + """The ADMM solution is that for which the merit value is the min + * sol: Iterate with the min merit value + * sol_val: Value of sol, according to the original objective + + Returns: + A tuple of (binary_vars, continuous_vars, sol_val), where + * binary_vars: binary variable values with the min merit value + * continuous_vars: continuous variable values with the min merit value + * sol_val: Value of the objective function + """ + + it_min_merits = self._state.merits.index(min(self._state.merits)) + binary_vars = self._state.x0_saved[it_min_merits] + continuous_vars = self._state.u_saved[it_min_merits] + sol_val = self._state.cost_iterates[it_min_merits] + return binary_vars, continuous_vars, sol_val + + def _update_lambda_mult(self) -> np.ndarray: + """ + Updates the values of lambda multiplier, given the updated iterates + x0, z, and y. + + Returns: The updated array of values of lambda multiplier. + + """ + return self._state.lambda_mult + \ + self._state.rho * (self._state.x0 - self._state.z - self._state.y) + + def _update_rho(self, primal_residual: float, dual_residual: float) -> None: + """Updating the rho parameter in ADMM. + + Args: + primal_residual: primal residual + dual_residual: dual residual + """ + + if self._params.vary_rho == UPDATE_RHO_BY_TEN_PERCENT: + # Increase rho, to aid convergence. + if self._state.rho < 1.e+10: + self._state.rho *= 1.1 + elif self._params.vary_rho == UPDATE_RHO_BY_RESIDUALS: + if primal_residual > self._params.mu_res * dual_residual: + self._state.rho = self._params.tau_incr * self._state.rho + elif dual_residual > self._params.mu_res * primal_residual: + self._state.rho = self._params.tau_decr * self._state.rho + + def _get_constraint_residual(self) -> float: + """Compute violation of the constraints of the original problem, as: + * norm 1 of the body-rhs of eq. constraints + * -1 * min(body - rhs, 0) for geq constraints + * max(body - rhs, 0) for leq constraints + + Returns: + Violation of the constraints as a float value + """ + solution = self._get_current_solution() + # equality constraints + cr_eq = 0 + for constraint in self._state.equality_constraints: + cr_eq += np.abs(constraint.evaluate(solution) - constraint.rhs) + + # inequality constraints + cr_ineq = 0 + for constraint in self._state.inequality_constraints: + sense = -1 if constraint.sense == Constraint.Sense.GE else 1 + cr_ineq += max(sense * (constraint.evaluate(solution) - constraint.rhs), 0) + + return cr_eq + cr_ineq + + def _get_merit(self, cost_iterate: float, constraint_residual: float) -> float: + """Compute merit value associated with the current iterate + + Args: + cost_iterate: Cost at the certain iteration. + constraint_residual: Value of violation of the constraints. + + Returns: + Merit value as a float + """ + return cost_iterate + self._params.mu_merit * constraint_residual + + def _get_objective_value(self) -> float: + """Computes the value of the objective function. + + Returns: + Value of the objective function as a float + """ + return self._state.op.objective.evaluate(self._get_current_solution()) + + def _get_solution_residuals(self, iteration: int) -> (float, float): + """Compute primal and dual residual. + + Args: + iteration: Iteration number. + + Returns: + r, s as primary and dual residuals. + """ + elements = self._state.x0 - self._state.z - self._state.y + primal_residual = np.linalg.norm(elements) + if iteration > 0: + elements_dual = self._state.z - self._state.z_saved[iteration - 1] + else: + elements_dual = self._state.z - self._state.z_init + dual_residual = self._state.rho * np.linalg.norm(elements_dual) + + return primal_residual, dual_residual diff --git a/qiskit/optimization/algorithms/cobyla_optimizer.py b/qiskit/optimization/algorithms/cobyla_optimizer.py new file mode 100644 index 000000000..36c5141e5 --- /dev/null +++ b/qiskit/optimization/algorithms/cobyla_optimizer.py @@ -0,0 +1,148 @@ + +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 COBYLA optimizer wrapped to be used within Qiskit Optimization.""" + +from typing import Optional + +import numpy as np +from scipy.optimize import fmin_cobyla + +from qiskit.optimization.algorithms import OptimizationAlgorithm, OptimizationResult +from qiskit.optimization.problems import QuadraticProgram, Constraint +from qiskit.optimization import QiskitOptimizationError, INFINITY + + +class CobylaOptimizer(OptimizationAlgorithm): + """The SciPy COBYLA optimizer wrapped as an Qiskit ``OptimizationAlgorithm``. + + This class provides a wrapper for ``scipy.optimize.fmin_cobyla`` + (https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.optimize.fmin_cobyla.html) + to be used within Qiskit Optimization. + The arguments for ``fmin_cobyla`` are passed via the constructor. + + Examples: + >>> problem = QuadraticProgram() + >>> # specify problem here + >>> optimizer = CobylaOptimizer() + >>> result = optimizer.solve(problem) + """ + + def __init__(self, rhobeg: float = 1.0, rhoend: float = 1e-4, maxfun: int = 1000, + disp: Optional[int] = None, catol: float = 2e-4) -> None: + """Initializes the CobylaOptimizer. + + This initializer takes the algorithmic parameters of COBYLA and stores them for later use + of ``fmin_cobyla`` when :meth:`solve` is invoked. + This optimizer can be applied to find a (local) optimum for problems consisting of only + continuous variables. + + Args: + rhobeg: Reasonable initial changes to the variables. + rhoend: Final accuracy in the optimization (not precisely guaranteed). + This is a lower bound on the size of the trust region. + disp: Controls the frequency of output; 0 implies no output. + Feasible values are {0, 1, 2, 3}. + maxfun: Maximum number of function evaluations. + catol: Absolute tolerance for constraint violations. + """ + + self._rhobeg = rhobeg + self._rhoend = rhoend + self._maxfun = maxfun + self._disp = disp + self._catol = catol + + def get_compatibility_msg(self, problem: QuadraticProgram) -> str: + """Checks whether a given problem can be solved with this optimizer. + + Checks whether the given problem is compatible, i.e., whether the problem contains only + continuous variables, and otherwise, returns a message explaining the incompatibility. + + Args: + problem: The optimization problem to check compatibility. + + Returns: + Returns a string describing the incompatibility. + """ + # check whether there are variables of type other than continuous + if len(problem.variables) > problem.get_num_continuous_vars(): + return 'The COBYLA optimizer supports only continuous variables' + + 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. + + 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. + """ + # check compatibility and raise exception if incompatible + msg = self.get_compatibility_msg(problem) + if len(msg) > 0: + raise QiskitOptimizationError('Incompatible problem: {}'.format(msg)) + + # construct quadratic objective function + def objective(x): + return problem.objective.sense.value * problem.objective.evaluate(x) + + # initialize constraints list + constraints = [] + + # add lower/upper bound constraints + for variable in problem.variables: + lowerbound = variable.lowerbound + upperbound = variable.upperbound + if lowerbound > -INFINITY: + constraints += [lambda x, lb=lowerbound: x - lb] + if upperbound < INFINITY: + constraints += [lambda x, ub=upperbound: ub - x] + + # pylint: disable=no-member + # add linear and quadratic constraints + for constraint in problem.linear_constraints + problem.quadratic_constraints: + rhs = constraint.rhs + sense = constraint.sense + + if sense == Constraint.Sense.EQ: + constraints += [ + lambda x, rhs=rhs, c=constraint: rhs - c.evaluate(x), + lambda x, rhs=rhs, c=constraint: c.evaluate(x) - rhs + ] + elif sense == Constraint.Sense.LE: + constraints += [lambda x, rhs=rhs, c=constraint: rhs - c.evaluate(x)] + elif sense == Constraint.Sense.GE: + constraints += [lambda x, rhs=rhs, c=constraint: c.evaluate(x) - rhs] + else: + raise QiskitOptimizationError('Unsupported constraint type!') + + # TODO: derive x_0 from lower/upper bounds + x_0 = np.zeros(len(problem.variables)) + + # run optimization + x = fmin_cobyla(objective, x_0, constraints, rhobeg=self._rhobeg, rhoend=self._rhoend, + maxfun=self._maxfun, disp=self._disp, catol=self._catol) + fval = problem.objective.sense.value * objective(x) + + # return results + return OptimizationResult(x, fval, x) diff --git a/qiskit/optimization/algorithms/cplex_optimizer.py b/qiskit/optimization/algorithms/cplex_optimizer.py new file mode 100644 index 000000000..6966aa0da --- /dev/null +++ b/qiskit/optimization/algorithms/cplex_optimizer.py @@ -0,0 +1,134 @@ + +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 Optimization.""" + +from typing import Optional +import logging + +from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult +from ..exceptions import QiskitOptimizationError +from ..problems.quadratic_program import QuadraticProgram + +logger = logging.getLogger(__name__) + +_HAS_CPLEX = False +try: + from cplex.exceptions import CplexSolverError + _HAS_CPLEX = True +except ImportError: + logger.info('CPLEX is not installed.') + + +class CplexOptimizer(OptimizationAlgorithm): + """The CPLEX optimizer wrapped as an Qiskit ``OptimizationAlgorithm``. + + This class provides a wrapper for ``cplex.Cplex`` (https://pypi.org/project/cplex/) + to be used within Qiskit Optimization. + + Examples: + >>> problem = QuadraticProgram() + >>> # specify problem here + >>> optimizer = CplexOptimizer() + >>> result = optimizer.solve(problem) + """ + + def __init__(self, disp: Optional[bool] = False) -> None: + """Initializes the CplexOptimizer. + + Args: + disp: Whether to print CPLEX output or not. + + Raises: + NameError: CPLEX is not installed. + """ + if not _HAS_CPLEX: + raise NameError('CPLEX is not installed.') + + self._disp = disp + + @property + def disp(self) -> bool: + """Returns the display setting. + + Returns: + Whether to print CPLEX 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 CPLEX accepts all problems that can be modeled using the + ``QuadraticProgram``. CPLEX may throw an exception in case the problem is determined + to be non-convex. + + 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 CPLEX problem + cplex = problem.to_docplex().get_cplex() + + # set display setting + if not self.disp: + cplex.set_log_stream(None) + cplex.set_error_stream(None) + cplex.set_warning_stream(None) + cplex.set_results_stream(None) + + # solve problem + try: + cplex.solve() + except CplexSolverError: + raise QiskitOptimizationError('Non convex/symmetric matrix.') + + # process results + sol = cplex.solution + + # create results + result = OptimizationResult(sol.get_values(), sol.get_objective_value(), sol) + + # return solution + return result diff --git a/qiskit/optimization/algorithms/grover_optimizer.py b/qiskit/optimization/algorithms/grover_optimizer.py new file mode 100644 index 000000000..4c95506d2 --- /dev/null +++ b/qiskit/optimization/algorithms/grover_optimizer.py @@ -0,0 +1,404 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""GroverOptimizer module""" + +import logging +from typing import Optional, Dict, Union, Tuple +import math +import random +import numpy as np +from qiskit import QuantumCircuit +from qiskit.providers import BaseBackend +from qiskit.aqua import QuantumInstance +from qiskit.optimization import QiskitOptimizationError +from qiskit.optimization.algorithms import OptimizationAlgorithm +from qiskit.optimization.problems import QuadraticProgram +from qiskit.optimization.converters import (QuadraticProgramToQubo, + QuadraticProgramToNegativeValueOracle) +from qiskit.optimization.algorithms.optimization_algorithm import OptimizationResult +from qiskit.aqua.algorithms.amplitude_amplifiers.grover import Grover + + +logger = logging.getLogger(__name__) + + +class GroverOptimizer(OptimizationAlgorithm): + """Uses Grover Adaptive Search (GAS) to find the minimum of a QUBO function.""" + + def __init__(self, num_value_qubits: int, num_iterations: int = 3, + quantum_instance: Optional[Union[BaseBackend, QuantumInstance]] = None) -> None: + """ + Args: + num_value_qubits: The number of value qubits. + num_iterations: The number of iterations the algorithm will search with + no improvement. + quantum_instance: Instance of selected backend, defaults to Aer's statevector simulator. + """ + self._num_value_qubits = num_value_qubits + self._n_iterations = num_iterations + self._quantum_instance = None + + if quantum_instance is not None: + self.quantum_instance = quantum_instance + + @property + def quantum_instance(self) -> QuantumInstance: + """The quantum instance to run the circuits. + + Returns: + The quantum instance used in the algorithm. + """ + return self._quantum_instance + + @quantum_instance.setter + def quantum_instance(self, quantum_instance: Union[BaseBackend, QuantumInstance]) -> None: + """Set the quantum instance used to run the circuits. + + Args: + quantum_instance: The quantum instance to be used in the algorithm. + """ + if isinstance(quantum_instance, BaseBackend): + self._quantum_instance = QuantumInstance(quantum_instance) + else: + self._quantum_instance = quantum_instance + + def get_compatibility_msg(self, problem: QuadraticProgram) -> str: + """Checks whether a given problem can be solved with this optimizer. + + Checks whether the given problem is compatible, i.e., whether the problem can be converted + to a QUBO, and otherwise, returns a message explaining the incompatibility. + + Args: + problem: The optimization problem to check compatibility. + + Returns: + A message describing the incompatibility. + """ + return QuadraticProgramToQubo.get_compatibility_msg(problem) + + def solve(self, problem: QuadraticProgram) -> OptimizationResult: + """Tries to solves the given problem using the grover optimizer. + + Runs the optimizer to try to solve the optimization problem. If the problem cannot be, + converted to a QUBO, this optimizer raises an exception due to incompatibility. + + Args: + problem: The problem to be solved. + + Returns: + The result of the optimizer applied to the problem. + + Raises: + AttributeError: If the quantum instance has not been set. + QiskitOptimizationError: If the problem is incompatible with the optimizer. + """ + if self.quantum_instance is None: + raise AttributeError('The quantum instance or backend has not been set.') + + # check compatibility and raise exception if incompatible + msg = self.get_compatibility_msg(problem) + if len(msg) > 0: + raise QiskitOptimizationError('Incompatible problem: {}'.format(msg)) + + # convert problem to QUBO + qubo_converter = QuadraticProgramToQubo() + problem_ = qubo_converter.encode(problem) + + # Variables for tracking the optimum. + optimum_found = False + optimum_key = math.inf + optimum_value = math.inf + threshold = 0 + n_key = len(problem_.variables) + n_value = self._num_value_qubits + + # Variables for tracking the solutions encountered. + num_solutions = 2**n_key + keys_measured = [] + + # Variables for result object. + func_dict = {} + operation_count = {} + iteration = 0 + + # Variables for stopping if we've hit the rotation max. + rotations = 0 + max_rotations = int(np.ceil(100*np.pi/4)) + + # Initialize oracle helper object. + orig_constant = problem_.objective.constant + measurement = not self._quantum_instance.is_statevector + opt_prob_converter = QuadraticProgramToNegativeValueOracle(n_value, + measurement) + + while not optimum_found: + m = 1 + improvement_found = False + + # Get oracle O and the state preparation operator A for the current threshold. + problem_.objective.constant = orig_constant - threshold + a_operator, oracle, func_dict = opt_prob_converter.encode(problem_) + + # Iterate until we measure a negative. + loops_with_no_improvement = 0 + while not improvement_found: + # Determine the number of rotations. + loops_with_no_improvement += 1 + rotation_count = int(np.ceil(random.uniform(0, m-1))) + rotations += rotation_count + + # Apply Grover's Algorithm to find values below the threshold. + if rotation_count > 0: + # TODO: Utilize Grover's incremental feature - requires changes to Grover. + grover = Grover(oracle, init_state=a_operator, num_iterations=rotation_count) + circuit = grover.construct_circuit( + measurement=self._quantum_instance.is_statevector) + else: + circuit = a_operator._circuit + + # Get the next outcome. + outcome = self._measure(circuit, n_key, n_value) + k = int(outcome[0:n_key], 2) + v = outcome[n_key:n_key + n_value] + int_v = self._bin_to_int(v, n_value) + threshold + v = self._twos_complement(int_v, n_value) + logger.info('Outcome: %s', outcome) + logger.info('Value: %s = %s', v, int_v) + + # If the value is an improvement, we update the iteration parameters (e.g. oracle). + if int_v < optimum_value: + optimum_key = k + optimum_value = int_v + logger.info('Current Optimum Key: %s', optimum_key) + logger.info('Current Optimum Value: %s', optimum_value) + if v.startswith('1'): + improvement_found = True + threshold = optimum_value + else: + # Using Durr and Hoyer method, increase m. + m = int(np.ceil(min(m * 8/7, 2**(n_key / 2)))) + logger.info('No Improvement. M: %s', m) + + # Check if we've already seen this value. + if k not in keys_measured: + keys_measured.append(k) + + # Assume the optimal if any of the stop parameters are true. + if loops_with_no_improvement >= self._n_iterations or \ + len(keys_measured) == num_solutions or rotations >= max_rotations: + improvement_found = True + optimum_found = True + + # Track the operation count. + operations = circuit.count_ops() + operation_count[iteration] = operations + iteration += 1 + logger.info('Operation Count: %s\n', operations) + + # Get original key and value pairs. + func_dict[-1] = orig_constant + solutions = self._get_qubo_solutions(func_dict, n_key) + + # If the constant is 0 and we didn't find a negative, the answer is likely 0. + if optimum_value >= 0 and orig_constant == 0: + optimum_key = 0 + opt_x = [1 if s == '1' else 0 for s in ('{0:%sb}' % n_key).format(optimum_key)] + + # Build the results object. + grover_results = GroverOptimizationResults(operation_count, n_key, n_value, func_dict) + result = OptimizationResult(x=opt_x, fval=solutions[optimum_key], + results={"grover_results": grover_results, + "qubo_converter": qubo_converter}) + + # cast binaries back to integers + result = qubo_converter.decode(result) + + return result + + def _measure(self, circuit: QuantumCircuit, n_key: int, n_value: int) -> str: + """Get probabilities from the given backend, and picks a random outcome.""" + probs = self._get_probs(n_key, n_value, circuit) + freq = sorted(probs.items(), key=lambda x: x[1], reverse=True) + + # Pick a random outcome. + freq[len(freq)-1] = (freq[len(freq)-1][0], 1 - sum([x[1] for x in freq[0:len(freq)-1]])) + idx = np.random.choice(len(freq), 1, p=[x[1] for x in freq])[0] + logger.info('Frequencies: %s', freq) + + return freq[idx][0] + + def _get_probs(self, n_key: int, n_value: int, qc: QuantumCircuit) -> Dict[str, float]: + """Gets probabilities from a given backend.""" + # Execute job and filter results. + result = self._quantum_instance.execute(qc) + if self._quantum_instance.is_statevector: + state = np.round(result.get_statevector(qc), 5) + keys = [bin(i)[2::].rjust(int(np.log2(len(state))), '0')[::-1] + for i in range(0, len(state))] + probs = [np.round(abs(a)*abs(a), 5) for a in state] + f_hist = dict(zip(keys, probs)) + hist = {} + for key in f_hist: + new_key = key[:n_key] + key[n_key:n_key+n_value][::-1] + key[n_key+n_value:] + hist[new_key] = f_hist[key] + else: + state = result.get_counts(qc) + shots = self._quantum_instance.run_config.shots + hist = {} + for key in state: + hist[key[:n_key] + key[n_key:n_key+n_value][::-1] + key[n_key+n_value:]] = \ + state[key] / shots + hist = dict(filter(lambda p: p[1] > 0, hist.items())) + + return hist + + @staticmethod + def _twos_complement(v: int, n_bits: int) -> str: + """Converts an integer into a binary string of n bits using two's complement.""" + assert -2**n_bits <= v < 2**n_bits + + if v < 0: + v += 2**n_bits + bin_v = bin(v)[2:] + else: + format_string = '{0:0'+str(n_bits)+'b}' + bin_v = format_string.format(v) + + return bin_v + + @staticmethod + def _bin_to_int(v: str, num_value_bits: int) -> int: + """Converts a binary string of n bits using two's complement to an integer.""" + if v.startswith("1"): + int_v = int(v, 2) - 2 ** num_value_bits + else: + int_v = int(v, 2) + + return int_v + + @staticmethod + def _get_qubo_solutions(function_dict: Dict[Union[int, Tuple[int, int]], int], n_key: int, + print_solutions: Optional[bool] = False): + """ Calculates all of the outputs of a QUBO function representable by n key qubits. + + Args: + function_dict: A dictionary representation of the function, where the keys correspond + to a variable, and the values are the corresponding coefficients. + n_key: The number of key qubits. + print_solutions: If true, the solutions will be formatted and printed. + + Returns: + dict: A dictionary of the inputs (keys) and outputs (values) of the QUBO function. + """ + # Determine constant. + constant = 0 + if -1 in function_dict: + constant = function_dict[-1] + format_string = '{0:0'+str(n_key)+'b}' + + # Iterate through every key combination. + if print_solutions: + print("QUBO Solutions:") + print("==========================") + solutions = {} + for i in range(2**n_key): + solution = constant + + # Convert int to a list of binary variables. + bin_key = format_string.format(i) + bin_list = [int(bin_key[j]) for j in range(len(bin_key))] + + # Handle the linear terms. + for k in range(len(bin_key)): + if bin_list[k] == 1 and k in function_dict: + solution += function_dict[k] + + # Handle the quadratic terms. + for j in range(len(bin_key)): + for q in range(len(bin_key)): + if (j, q) in function_dict and j != q and bin_list[j] == 1 and bin_list[q] == 1: + solution += function_dict[(j, q)] + + # Print row. + if print_solutions: + spacer = "" if i >= 10 else " " + value_spacer = " " if solution < 0 else " " + print(spacer + str(i), "=", bin_key, "->" + value_spacer + str(round(solution, 4))) + + # Record solution. + solutions[i] = solution + + if print_solutions: + print() + + return solutions + + +class GroverOptimizationResults: + """A results object for Grover Optimization methods.""" + + def __init__(self, operation_counts: Dict[int, Dict[str, int]], + n_input_qubits: int, n_output_qubits: int, + func_dict: Dict[Union[int, Tuple[int, int]], int]) -> None: + """ + Args: + operation_counts: The counts of each operation performed per iteration. + n_input_qubits: The number of qubits used to represent the input. + n_output_qubits: The number of qubits used to represent the output. + func_dict: A dictionary representation of the function, where the keys correspond + to a variable, and the values are the corresponding coefficients. + """ + self._operation_counts = operation_counts + self._n_input_qubits = n_input_qubits + self._n_output_qubits = n_output_qubits + self._func_dict = func_dict + + @property + def operation_counts(self) -> Dict[int, Dict[str, int]]: + """Get the operation counts. + + Returns: + The counts of each operation performed per iteration. + """ + return self._operation_counts + + @property + def n_input_qubits(self) -> int: + """Getter of n_input_qubits + + Returns: + The number of qubits used to represent the input. + """ + return self._n_input_qubits + + @property + def n_output_qubits(self) -> int: + """Getter of n_output_qubits + + Returns: + The number of qubits used to represent the output. + """ + return self._n_output_qubits + + @property + def func_dict(self) -> Dict[Union[int, Tuple[int, int]], int]: + """Getter of func_dict + + Returns: + A dictionary of coefficients describing a function, where the keys are the subscripts + of the variables (e.g. x1), and the values are the corresponding coefficients. If there + is a constant term, it is referenced by key -1. + """ + return self._func_dict diff --git a/qiskit/optimization/algorithms/minimum_eigen_optimizer.py b/qiskit/optimization/algorithms/minimum_eigen_optimizer.py new file mode 100644 index 000000000..81ffa8b66 --- /dev/null +++ b/qiskit/optimization/algorithms/minimum_eigen_optimizer.py @@ -0,0 +1,247 @@ + +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""A wrapper for minimum eigen solvers from Qiskit Aqua to be used within Qiskit Optimization.""" + +from typing import Optional, Any, Union, Tuple, List +import numpy as np + +from qiskit import QuantumCircuit, BasicAer, execute +from qiskit.aqua.algorithms import MinimumEigensolver +from qiskit.aqua.operators import WeightedPauliOperator, MatrixOperator + +from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult +from ..problems.quadratic_program import QuadraticProgram +from ..converters.quadratic_program_to_operator import QuadraticProgramToOperator +from ..converters.quadratic_program_to_qubo import QuadraticProgramToQubo +from .. import QiskitOptimizationError + + +class MinimumEigenOptimizerResult(OptimizationResult): + """ Minimum Eigen Optimizer Result.""" + + def __init__(self, x: Optional[Any] = None, fval: Optional[Any] = None, + samples: Optional[Any] = None, results: Optional[Any] = None) -> None: + super().__init__(x, fval, results) + self._samples = samples + + @property + def samples(self) -> Any: + """ returns samples """ + return self._samples + + @samples.setter + def samples(self, samples: Any) -> None: + """ set samples """ + self._samples = samples + + def get_correlations(self): + """ get correlation matrix from samples """ + + states = [v[0] for v in self.samples] + probs = [v[2] for v in self.samples] + + n = len(states[0]) + correlations = np.zeros((n, n)) + for k, prob in enumerate(probs): + b = states[k] + for i in range(n): + for j in range(i): + if b[i] == b[j]: + correlations[i, j] += prob + else: + correlations[i, j] -= prob + return correlations + + +class MinimumEigenOptimizer(OptimizationAlgorithm): + """A wrapper for minimum eigen solvers from Qiskit Aqua to be used within Qiskit Optimization. + + This class provides a wrapper for minimum eigen solvers from Qiskit Aqua. + It assumes a problem consisting only of binary or integer variables as well as linear equality + constraints thereof. It converts such a problem into a Quadratic Unconstrained Binary + Optimization (QUBO) problem by expanding integer variables into binary variables and by adding + the linear equality constraints as weighted penalty terms to the objective function. The + resulting QUBO is then translated into an Ising Hamiltonian whose minimal eigen vector and + corresponding eigenstate correspond to the optimal solution of the original optimization + problem. The provided minimum eigen solver is then used to approximate the ground state of the + Hamiltonian to find a good solution for the optimization problem. + + Examples: + >>> problem = QuadraticProgram() + >>> # specify problem here + >>> # specify minimum eigen solver to be used, e.g., QAOA + >>> qaoa = QAOA(...) + >>> optimizer = MinEigenOptimizer(qaoa) + >>> result = optimizer.solve(problem) + """ + + def __init__(self, min_eigen_solver: MinimumEigensolver, penalty: Optional[float] = None + ) -> None: + """ + This initializer takes the minimum eigen solver to be used to approximate the ground state + of the resulting Hamiltonian as well as a optional penalty factor to scale penalty terms + representing linear equality constraints. If no penalty factor is provided, a default + is computed during the algorithm (TODO). + + Args: + min_eigen_solver: The eigen solver to find the ground state of the Hamiltonian. + penalty: The penalty factor to be used, or ``None`` for applying a default logic. + """ + self._min_eigen_solver = min_eigen_solver + self._penalty = penalty + + def get_compatibility_msg(self, problem: QuadraticProgram) -> str: + """Checks whether a given problem can be solved with this optimizer. + + Checks whether the given problem is compatible, i.e., whether the problem can be converted + to a QUBO, and otherwise, returns a message explaining the incompatibility. + + Args: + problem: The optimization problem to check compatibility. + + Returns: + A message describing the incompatibility. + """ + return QuadraticProgramToQubo.get_compatibility_msg(problem) + + def solve(self, problem: QuadraticProgram) -> MinimumEigenOptimizerResult: + """Tries to solves the given problem using the optimizer. + + Runs the optimizer to try to solve the optimization problem. + + Args: + problem: The problem to be solved. + + Returns: + The result of the optimizer applied to the problem. + + Raises: + QiskitOptimizationError: If problem not compatible. + """ + # check compatibility and raise exception if incompatible + msg = self.get_compatibility_msg(problem) + if len(msg) > 0: + raise QiskitOptimizationError('Incompatible problem: {}'.format(msg)) + + # convert problem to QUBO + qubo_converter = QuadraticProgramToQubo() + problem_ = qubo_converter.encode(problem) + + # construct operator and offset + operator_converter = QuadraticProgramToOperator() + operator, offset = operator_converter.encode(problem_) + + # approximate ground state of operator using min eigen solver + eigen_results = self._min_eigen_solver.compute_minimum_eigenvalue(operator) + + # analyze results + samples = eigenvector_to_solutions(eigen_results.eigenstate, operator) + samples = [(res[0], problem_.objective.sense.value * (res[1] + offset), res[2]) + for res in samples] + samples.sort(key=lambda x: problem_.objective.sense.value * x[1]) + + # translate result back to integers + opt_res = MinimumEigenOptimizerResult(samples[0][0], samples[0][1], samples, qubo_converter) + opt_res = qubo_converter.decode(opt_res) + + # translate results back to original problem + return opt_res + + +def eigenvector_to_solutions(eigenvector: Union[dict, np.ndarray], + operator: Union[WeightedPauliOperator, MatrixOperator], + min_probability: float = 1e-6) -> List[Tuple[str, float, float]]: + """Convert the eigenvector to the bitstrings and corresponding eigenvalues. + + Examples: + >>> op = MatrixOperator(numpy.array([[1, 1], [1, -1]]) / numpy.sqrt(2)) + >>> eigenvectors = {'0': 12, '1': 1} + >>> print(eigenvector_to_solutions(eigenvectors, op)) + [('0', 0.7071067811865475, 0.9230769230769231), ('1', -0.7071067811865475, 0.07692307692307693)] + + >>> op = MatrixOperator(numpy.array([[1, 1], [1, -1]]) / numpy.sqrt(2)) + >>> eigenvectors = numpy.array([1, 1] / numpy.sqrt(2), dtype=complex) + >>> print(eigenvector_to_solutions(eigenvectors, op)) + [('0', 0.7071067811865475, 0.4999999999999999), ('1', -0.7071067811865475, 0.4999999999999999)] + + Returns: + For each computational basis state contained in the eigenvector, return the basis + state as bitstring along with the operator evaluated at that bitstring and the + probability of sampling this bitstring from the eigenvector. + + Raises: + TypeError: Invalid Argument + + """ + solutions = [] + if isinstance(eigenvector, dict): + all_counts = sum(eigenvector.values()) + # iterate over all samples + for bitstr, count in eigenvector.items(): + sampling_probability = count / all_counts + # add the bitstring, if the sampling probability exceeds the threshold + if sampling_probability > 0: + if sampling_probability >= min_probability: + value = eval_operator_at_bitstring(operator, bitstr) + solutions += [(bitstr, value, sampling_probability)] + + elif isinstance(eigenvector, np.ndarray): + num_qubits = int(np.log2(eigenvector.size)) + probabilities = np.abs(eigenvector * eigenvector.conj()) + + # iterate over all states and their sampling probabilities + for i, sampling_probability in enumerate(probabilities): + + # add the i-th state if the sampling probability exceeds the threshold + if sampling_probability > 0: + if sampling_probability >= min_probability: + bitstr = '{:b}'.format(i).rjust(num_qubits, '0')[::-1] + value = eval_operator_at_bitstring(operator, bitstr) + solutions += [(bitstr, value, sampling_probability)] + + else: + raise TypeError('Unsupported format of eigenvector. Provide a dict or numpy.ndarray.') + + return solutions + + +def eval_operator_at_bitstring(operator: Union[WeightedPauliOperator, MatrixOperator], + bitstr: str) -> float: + """Evaluate an Aqua operator at a given bitstring. + + This simulates a circuit representing the bitstring. Note that this will not be needed + with the Operator logic introduced in 0.7.0. + + Args: + operator: The operator which is evaluated. + bitstr: The bitstring at which the operator is evaluated. + + Returns: + The operator evaluated with the quantum state the bitstring describes. + """ + # TODO check that operator size and bitstr are compatible + circuit = QuantumCircuit(len(bitstr)) + for i, bit in enumerate(bitstr): + if bit == '1': + circuit.x(i) + + # simulate the circuit + result = execute(circuit, BasicAer.get_backend('statevector_simulator')).result() + + # evaluate the operator + value = np.real(operator.evaluate_with_statevector(result.get_statevector())[0]) + + return value diff --git a/qiskit/optimization/algorithms/optimization_algorithm.py b/qiskit/optimization/algorithms/optimization_algorithm.py new file mode 100644 index 000000000..48cf97ffd --- /dev/null +++ b/qiskit/optimization/algorithms/optimization_algorithm.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""An abstract class for optimization algorithms in Qiskit Optimization.""" + +from abc import ABC, abstractmethod +from enum import Enum +from typing import Any, Optional + +from ..problems.quadratic_program import QuadraticProgram + + +class OptimizationAlgorithm(ABC): + """An abstract class for optimization algorithms in Qiskit Optimization.""" + + @abstractmethod + def get_compatibility_msg(self, problem: QuadraticProgram) -> str: + """Checks whether a given problem can be solved with the optimizer implementing this method. + + Args: + problem: The optimization problem to check compatibility. + + Returns: + Returns the incompatibility message. If the message is empty no issues were found. + """ + + def is_compatible(self, problem: QuadraticProgram) -> bool: + """Checks whether a given problem can be solved with the optimizer implementing this method. + + Args: + problem: The optimization problem to check compatibility. + + Returns: + Returns True if the problem is compatible, False otherwise. + """ + return len(self.get_compatibility_msg(problem)) == 0 + + @abstractmethod + 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. + + 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. + """ + raise NotImplementedError + + +class OptimizationResultStatus(Enum): + """Feasible values for the termination status of an optimization algorithm.""" + SUCCESS = 0 + FAILURE = 1 + INFEASIBLE = 2 + + +class OptimizationResult: + """The optimization result class. + + The optimization algorithms return an object of the type `OptimizationResult`, which enforces + providing the following attributes. + + Attributes: + x: The optimal value found in the optimization algorithm. + fval: The function value corresponding to the optimal value. + results: The original results object returned from the optimization algorithm. This can + contain more information than only the optimal value and function value. + status: The termination status of the algorithm. + """ + + Status = OptimizationResultStatus + + def __init__(self, x: Optional[Any] = None, fval: Optional[Any] = None, + results: Optional[Any] = None, + status: OptimizationResultStatus = OptimizationResultStatus.SUCCESS) -> None: + self._val = x + self._fval = fval + self._results = results + self._status = status + + def __repr__(self): + return '([{}] / {} / {})'.format(','.join([str(x_) for x_ in self.x]), self.fval, + self.status) + + def __str__(self): + return 'x=[{}], fval={}'.format(','.join([str(x_) for x_ in self.x]), self.fval) + + @property + def x(self) -> Any: + """Returns the optimal value found in the optimization. + + Returns: + The optimal value found in the optimization. + """ + return self._val + + @property + def fval(self) -> Any: + """Returns the optimal function value. + + Returns: + The function value corresponding to the optimal value found in the optimization. + """ + return self._fval + + @property + def results(self) -> Any: + """Return the original results object from the algorithm. + + Currently a dump for any leftovers. + + Returns: + Additional result information of the optimization algorithm. + """ + return self._results + + @property + def status(self) -> OptimizationResultStatus: + """Return the termination status of the algorithm. + + Returns: + The termination status of the algorithm. + """ + return self._status + + @x.setter + def x(self, x: Any) -> None: + """Set a new optimal value. + + Args: + x: The new optimal value. + """ + self._val = x + + @fval.setter + def fval(self, fval: Any) -> None: + """Set a new optimal function value. + + Args: + fval: The new optimal function value. + """ + self._fval = fval + + @results.setter + def results(self, results: Any) -> None: + """Set results. + + Args: + results: The new additional results of the optimization. + """ + self._results = results + + @status.setter + def status(self, status: OptimizationResultStatus) -> None: + """Set a new termination status. + + Args: + status: The new termination status. + """ + self._status = status diff --git a/qiskit/optimization/algorithms/recursive_minimum_eigen_optimizer.py b/qiskit/optimization/algorithms/recursive_minimum_eigen_optimizer.py new file mode 100644 index 000000000..ec6bdd745 --- /dev/null +++ b/qiskit/optimization/algorithms/recursive_minimum_eigen_optimizer.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""A recursive minimal eigen optimizer in Qiskit Optimization.""" + +from copy import deepcopy +from typing import Optional +import logging +import numpy as np + +from qiskit.aqua.algorithms import NumPyMinimumEigensolver +from qiskit.aqua.utils.validation import validate_min + +from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult +from .minimum_eigen_optimizer import MinimumEigenOptimizer +from ..exceptions import QiskitOptimizationError +from ..problems.quadratic_program import QuadraticProgram +from ..converters.quadratic_program_to_qubo import QuadraticProgramToQubo + +logger = logging.getLogger(__name__) + + +class RecursiveMinimumEigenOptimizer(OptimizationAlgorithm): + """A meta-algorithm that applies a recursive optimization. + + The recursive minimum eigen optimizer applies a recursive optimization on top of + :class:`~qiskit.optimization.algorithms.MinimumEigenOptimizer`. + The algorithm is introduced in [1]. + + Examples: + >>> problem = QuadraticProgram() + >>> # specify problem here + >>> # specify minimum eigen solver to be used, e.g., QAOA + >>> qaoa = QAOA(...) + >>> optimizer = RecursiveMinEigenOptimizer(qaoa) + >>> result = optimizer.solve(problem) + + References: + [1]: Bravyi et al. (2019), Obstacles to State Preparation and Variational Optimization + from Symmetry Protection. http://arxiv.org/abs/1910.08980. + """ + + def __init__(self, min_eigen_optimizer: MinimumEigenOptimizer, min_num_vars: int = 1, + min_num_vars_optimizer: Optional[OptimizationAlgorithm] = None, + penalty: Optional[float] = None) -> None: + """ Initializes the recursive minimum eigen optimizer. + + This initializer takes a ``MinimumEigenOptimizer``, the parameters to specify until when to + to apply the iterative scheme, and the optimizer to be applied once the threshold number of + variables is reached. + + Args: + min_eigen_optimizer: The eigen optimizer to use in every iteration. + min_num_vars: The minimum number of variables to apply the recursive scheme. If this + threshold is reached, the min_num_vars_optimizer is used. + min_num_vars_optimizer: This optimizer is used after the recursive scheme for the + problem with the remaining variables. + penalty: The factor that is used to scale the penalty terms corresponding to linear + equality constraints. + + TODO: add flag to store full history. + + Raises: + QiskitOptimizationError: In case of invalid parameters (num_min_vars < 1). + """ + + validate_min('min_num_vars', min_num_vars, 1) + + self._min_eigen_optimizer = min_eigen_optimizer + self._min_num_vars = min_num_vars + if min_num_vars_optimizer: + self._min_num_vars_optimizer = min_num_vars_optimizer + else: + self._min_num_vars_optimizer = MinimumEigenOptimizer(NumPyMinimumEigensolver()) + self._penalty = penalty + + def get_compatibility_msg(self, problem: QuadraticProgram) -> str: + """Checks whether a given problem can be solved with this optimizer. + + Checks whether the given problem is compatible, i.e., whether the problem can be converted + to a QUBO, and otherwise, returns a message explaining the incompatibility. + + Args: + problem: The optimization problem to check compatibility. + + Returns: + A message describing the incompatibility. + """ + return QuadraticProgramToQubo.get_compatibility_msg(problem) + + def solve(self, problem: QuadraticProgram) -> OptimizationResult: + """Tries to solve the given problem using the recursive optimizer. + + Runs the optimizer to try to solve the optimization problem. + + Args: + problem: The problem to be solved. + + Returns: + The result of the optimizer applied to the problem. + + Raises: + QiskitOptimizationError: Incompatible problem. + QiskitOptimizationError: Infeasible due to variable substitution + """ + # check compatibility and raise exception if incompatible + msg = self.get_compatibility_msg(problem) + if len(msg) > 0: + raise QiskitOptimizationError('Incompatible problem: {}'.format(msg)) + + # convert problem to QUBO, this implicitly checks if the problem is compatible + qubo_converter = QuadraticProgramToQubo() + problem_ = qubo_converter.encode(problem) + problem_ref = deepcopy(problem_) + + # run recursive optimization until the resulting problem is small enough + replacements = {} + while problem_.get_num_vars() > self._min_num_vars: + + # solve current problem with optimizer + result = self._min_eigen_optimizer.solve(problem_) + + # analyze results to get strongest correlation + correlations = result.get_correlations() + i, j = self._find_strongest_correlation(correlations) + + x_i = problem_.variables[i].name + x_j = problem_.variables[j].name + if correlations[i, j] > 0: + # set x_i = x_j + problem_.substitute_variables() + problem_ = problem_.substitute_variables(variables={i: (j, 1)}) + if problem_.status == QuadraticProgram.Status.INFEASIBLE: + raise QiskitOptimizationError('Infeasible due to variable substitution') + replacements[x_i] = (x_j, 1) + else: + # set x_i = 1 - x_j, this is done in two steps: + # 1. set x_i = 1 + x_i + # 2. set x_i = -x_j + + # 1a. get additional offset + constant = problem_.objective.constant + constant += problem_.objective.quadratic[i, i] + constant += problem_.objective.linear[i] + problem_.objective.constant = constant + + # 1b. get additional linear part + for k in range(problem_.get_num_vars()): + coeff = problem_.objective.quadratic[i, k] + if np.abs(coeff) > 1e-10: + coeff += problem_.objective.linear[k] + problem_.objective.linear[k] = coeff + + # 2. replace x_i by -x_j + problem_ = problem_.substitute_variables(variables={i: (j, -1)}) + if problem_.status == QuadraticProgram.Status.INFEASIBLE: + raise QiskitOptimizationError('Infeasible due to variable substitution') + replacements[x_i] = (x_j, -1) + + # solve remaining problem + result = self._min_num_vars_optimizer.solve(problem_) + + # unroll replacements + var_values = {} + for i, x in enumerate(problem_.variables): + var_values[x.name] = result.x[i] + + def find_value(x, replacements, var_values): + if x in var_values: + # if value for variable is known, return it + return var_values[x] + elif x in replacements: + # get replacement for variable + (y, sgn) = replacements[x] + # find details for replacing variable + value = find_value(y, replacements, var_values) + # construct, set, and return new value + var_values[x] = value if sgn == 1 else 1 - value + return var_values[x] + else: + raise QiskitOptimizationError('Invalid values!') + + # loop over all variables to set their values + for x_i in problem_ref.variables: + if x_i.name not in var_values: + find_value(x_i.name, replacements, var_values) + + # construct result + x = [var_values[x_aux.name] for x_aux in problem_ref.variables] + fval = result.fval + results = OptimizationResult(x, fval, (replacements, qubo_converter)) + results = qubo_converter.decode(results) + return results + + def _find_strongest_correlation(self, correlations): + m_max = np.argmax(np.abs(correlations.flatten())) + i = int(m_max // len(correlations)) + j = int(m_max - i*len(correlations)) + return (i, j) diff --git a/qiskit/optimization/applications/__init__.py b/qiskit/optimization/applications/__init__.py new file mode 100644 index 000000000..4317685c2 --- /dev/null +++ b/qiskit/optimization/applications/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. diff --git a/qiskit/optimization/ising/__init__.py b/qiskit/optimization/applications/ising/__init__.py similarity index 84% rename from qiskit/optimization/ising/__init__.py rename to qiskit/optimization/applications/ising/__init__.py index a3861fe01..b9869755a 100644 --- a/qiskit/optimization/ising/__init__.py +++ b/qiskit/optimization/applications/ising/__init__.py @@ -13,11 +13,11 @@ # that they have been altered from the originals. """ -Ising Models (:mod:`qiskit.optimization.ising`) -=============================================== +Ising Models (:mod:`qiskit.optimization.applications.ising`) +============================================================ Ising models for optimization problems -.. currentmodule:: qiskit.optimization.ising +.. currentmodule:: qiskit.optimization.applications.ising Ising Models ============ diff --git a/qiskit/optimization/ising/clique.py b/qiskit/optimization/applications/ising/clique.py similarity index 91% rename from qiskit/optimization/ising/clique.py rename to qiskit/optimization/applications/ising/clique.py index 56773d801..cdf160dd3 100644 --- a/qiskit/optimization/ising/clique.py +++ b/qiskit/optimization/applications/ising/clique.py @@ -12,8 +12,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -""" -Convert clique instances into Pauli list +"""Convert clique instances into Pauli list + Deal with Gset format. See https://web.stanford.edu/~yyye/yyye/Gset/ """ @@ -67,11 +67,11 @@ def get_operator(weight_matrix, K): # pylint: disable=invalid-name pauli_list = [] shift = 0 - Y = K - 0.5*num_nodes # Y = K-sum_{v}{1/2} + Y = K - 0.5 * num_nodes # Y = K-sum_{v}{1/2} A = 1000 # Ha part: - shift += A*Y*Y + shift += A * Y * Y for i in range(num_nodes): for j in range(num_nodes): @@ -80,16 +80,16 @@ def get_operator(weight_matrix, K): # pylint: disable=invalid-name zp = np.zeros(num_nodes, dtype=np.bool) zp[i] = True zp[j] = True - pauli_list.append([A*0.25, Pauli(zp, xp)]) + pauli_list.append([A * 0.25, Pauli(zp, xp)]) else: - shift += A*0.25 + shift += A * 0.25 for i in range(num_nodes): xp = np.zeros(num_nodes, dtype=np.bool) zp = np.zeros(num_nodes, dtype=np.bool) zp[i] = True - pauli_list.append([-A*Y, Pauli(zp, xp)]) + pauli_list.append([-A * Y, Pauli(zp, xp)]) - shift += 0.5*K*(K-1) + shift += 0.5 * K * (K - 1) for i in range(num_nodes): for j in range(i): @@ -128,7 +128,7 @@ def satisfy_or_not(x, w, K): # pylint: disable=invalid-name X = np.outer(x, x) w_01 = np.where(w != 0, 1, 0) - return np.sum(w_01 * X) == K*(K-1) # note sum() count the same edge twice + return np.sum(w_01 * X) == K * (K - 1) # note sum() count the same edge twice def get_graph_solution(x): diff --git a/qiskit/optimization/ising/common.py b/qiskit/optimization/applications/ising/common.py similarity index 97% rename from qiskit/optimization/ising/common.py rename to qiskit/optimization/applications/ising/common.py index 83f09ed13..8ad0b1a2b 100644 --- a/qiskit/optimization/ising/common.py +++ b/qiskit/optimization/applications/ising/common.py @@ -2,7 +2,7 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2019. +# (C) Copyright IBM 2018, 2020. # # 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 @@ -44,7 +44,7 @@ def random_graph(n, weight_range=10, edge_prob=0.3, negative_weight=True, w = np.zeros((n, n)) m = 0 for i in range(n): - for j in range(i+1, n): + for j in range(i + 1, n): if aqua_globals.random.rand() <= edge_prob: w[i, j] = aqua_globals.random.randint(1, weight_range) if aqua_globals.random.rand() >= 0.5 and negative_weight: @@ -55,7 +55,7 @@ def random_graph(n, weight_range=10, edge_prob=0.3, negative_weight=True, with open(savefile, 'w') as outfile: outfile.write('{} {}\n'.format(n, m)) for i in range(n): - for j in range(i+1, n): + for j in range(i + 1, n): if w[i, j] != 0: outfile.write('{} {} {}\n'.format(i + 1, j + 1, w[i, j])) return w @@ -76,7 +76,7 @@ def random_number_list(n, weight_range=100, savefile=None, seed=None): if seed: aqua_globals.random_seed = seed - number_list = aqua_globals.random.randint(low=1, high=(weight_range+1), size=n) + number_list = aqua_globals.random.randint(low=1, high=(weight_range + 1), size=n) if savefile: with open(savefile, 'w') as outfile: for i in range(n): diff --git a/qiskit/optimization/ising/docplex.py b/qiskit/optimization/applications/ising/docplex.py similarity index 91% rename from qiskit/optimization/ising/docplex.py rename to qiskit/optimization/applications/ising/docplex.py index a3113539b..b5807ece6 100644 --- a/qiskit/optimization/ising/docplex.py +++ b/qiskit/optimization/applications/ising/docplex.py @@ -2,7 +2,7 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2020 +# (C) Copyright IBM 2019, 2020. # # 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 @@ -59,9 +59,9 @@ """ +from typing import Tuple import logging from math import fsum -from typing import Tuple import numpy as np from docplex.mp.constants import ComparisonType @@ -76,18 +76,17 @@ def get_operator(mdl: Model, auto_penalty: bool = True, default_penalty: float = 1e5) -> Tuple[WeightedPauliOperator, float]: - """ - Generate Ising Hamiltonian from a model of DOcplex. + """Generate Ising Hamiltonian from a model of DOcplex. Args: - mdl : A model of DOcplex for a optimization problem. - auto_penalty : If true, the penalty coefficient is automatically defined + mdl: A model of DOcplex for a optimization problem. + auto_penalty: If true, the penalty coefficient is automatically defined by "_auto_define_penalty()". - default_penalty : The default value of the penalty coefficient for the constraints. + default_penalty: The default value of the penalty coefficient for the constraints. This value is used if "auto_penalty" is False. Returns: - A WeightedPauliOperator for the Hamiltonian and a constant shift for the obj function. + Operator for the Hamiltonian and a constant shift for the obj function. """ _validate_input_model(mdl) @@ -208,15 +207,14 @@ def get_operator(mdl: Model, auto_penalty: bool = True, return qubit_op, shift -def _validate_input_model(mdl: Model): - """ - Check whether an input model is valid. If not, raise an AquaError +def _validate_input_model(mdl: Model) -> None: + """Check whether an input model is valid. If not, raise an AquaError. Args: - mdl : A model of DOcplex for a optimization problem. + mdl: A model of DOcplex for a optimization problem. Raises: - AquaError: Unsupported input model + AquaError: Unsupported input model. """ valid = True @@ -242,14 +240,13 @@ def _validate_input_model(mdl: Model): def _auto_define_penalty(mdl: Model, default_penalty: float = 1e5) -> float: - """ - Automatically define the penalty coefficient. - This returns object function's (upper bound - lower bound + 1). + """Automatically define the penalty coefficient. + This returns object function's (upper bound - lower bound + 1). Args: - mdl : A model of DOcplex for a optimization problem. - default_penalty : The default value of the penalty coefficient for the constraints. + mdl: A model of DOcplex for a optimization problem. + default_penalty: The default value of the penalty coefficient for the constraints. Returns: The penalty coefficient for the Hamiltonian. diff --git a/qiskit/optimization/ising/exact_cover.py b/qiskit/optimization/applications/ising/exact_cover.py similarity index 97% rename from qiskit/optimization/ising/exact_cover.py rename to qiskit/optimization/applications/ising/exact_cover.py index 39f2928bf..87583e3f6 100644 --- a/qiskit/optimization/ising/exact_cover.py +++ b/qiskit/optimization/applications/ising/exact_cover.py @@ -47,7 +47,7 @@ def get_operator(list_of_subsets): U = [] for sub in list_of_subsets: U.extend(sub) - U = np.unique(U) # U is the universe + U = np.unique(U) # U is the universe shift = 0 pauli_list = [] @@ -57,8 +57,8 @@ def get_operator(list_of_subsets): cond = [True if e in sub else False for sub in list_of_subsets] indices_has_e = np.arange(n)[cond] num_has_e = len(indices_has_e) - Y = 1-0.5*num_has_e - shift += Y*Y + Y = 1 - 0.5 * num_has_e + shift += Y * Y for i in indices_has_e: for j in indices_has_e: diff --git a/qiskit/optimization/ising/graph_partition.py b/qiskit/optimization/applications/ising/graph_partition.py similarity index 99% rename from qiskit/optimization/ising/graph_partition.py rename to qiskit/optimization/applications/ising/graph_partition.py index c805783cf..9b556d9c5 100644 --- a/qiskit/optimization/ising/graph_partition.py +++ b/qiskit/optimization/applications/ising/graph_partition.py @@ -85,7 +85,7 @@ def objective_value(x, w): float: value of the cut. """ # pylint: disable=invalid-name - X = np.outer(x, (1-x)) + X = np.outer(x, (1 - x)) w_01 = np.where(w != 0, 1, 0) return np.sum(w_01 * X) diff --git a/qiskit/optimization/ising/knapsack.py b/qiskit/optimization/applications/ising/knapsack.py similarity index 100% rename from qiskit/optimization/ising/knapsack.py rename to qiskit/optimization/applications/ising/knapsack.py diff --git a/qiskit/optimization/ising/max_cut.py b/qiskit/optimization/applications/ising/max_cut.py similarity index 100% rename from qiskit/optimization/ising/max_cut.py rename to qiskit/optimization/applications/ising/max_cut.py diff --git a/qiskit/optimization/ising/partition.py b/qiskit/optimization/applications/ising/partition.py similarity index 96% rename from qiskit/optimization/ising/partition.py rename to qiskit/optimization/applications/ising/partition.py index 396d3083b..c305b02fc 100644 --- a/qiskit/optimization/ising/partition.py +++ b/qiskit/optimization/applications/ising/partition.py @@ -52,7 +52,7 @@ def get_operator(values): z_p[i] = True z_p[j] = True pauli_list.append([2. * values[i] * values[j], Pauli(z_p, x_p)]) - return WeightedPauliOperator(paulis=pauli_list), sum(values*values) + return WeightedPauliOperator(paulis=pauli_list), sum(values * values) def partition_value(x, number_list): diff --git a/qiskit/optimization/ising/set_packing.py b/qiskit/optimization/applications/ising/set_packing.py similarity index 92% rename from qiskit/optimization/ising/set_packing.py rename to qiskit/optimization/applications/ising/set_packing.py index 436930a1b..aafc666c4 100644 --- a/qiskit/optimization/ising/set_packing.py +++ b/qiskit/optimization/applications/ising/set_packing.py @@ -60,17 +60,17 @@ def get_operator(list_of_subsets): vp = np.zeros(n) vp[i] = 1 vp[j] = 1 - pauli_list.append([A*0.25, Pauli(vp, wp)]) + pauli_list.append([A * 0.25, Pauli(vp, wp)]) vp2 = np.zeros(n) vp2[i] = 1 - pauli_list.append([A*0.25, Pauli(vp2, wp)]) + pauli_list.append([A * 0.25, Pauli(vp2, wp)]) vp3 = np.zeros(n) vp3[j] = 1 - pauli_list.append([A*0.25, Pauli(vp3, wp)]) + pauli_list.append([A * 0.25, Pauli(vp3, wp)]) - shift += A*0.25 + shift += A * 0.25 for i in range(n): wp = np.zeros(n) diff --git a/qiskit/optimization/ising/stable_set.py b/qiskit/optimization/applications/ising/stable_set.py similarity index 94% rename from qiskit/optimization/ising/stable_set.py rename to qiskit/optimization/applications/ising/stable_set.py index 28e0859c9..fd36adc49 100644 --- a/qiskit/optimization/ising/stable_set.py +++ b/qiskit/optimization/applications/ising/stable_set.py @@ -45,7 +45,7 @@ def get_operator(w): pauli_list = [] shift = 0 for i in range(num_nodes): - for j in range(i+1, num_nodes): + for j in range(i + 1, num_nodes): if w[i, j] != 0: x_p = np.zeros(num_nodes, dtype=np.bool) z_p = np.zeros(num_nodes, dtype=np.bool) @@ -58,8 +58,8 @@ def get_operator(w): x_p = np.zeros(num_nodes, dtype=np.bool) z_p = np.zeros(num_nodes, dtype=np.bool) z_p[i] = True - pauli_list.append([degree - 1/2, Pauli(z_p, x_p)]) - return WeightedPauliOperator(paulis=pauli_list), shift - num_nodes/2 + pauli_list.append([degree - 1 / 2, Pauli(z_p, x_p)]) + return WeightedPauliOperator(paulis=pauli_list), shift - num_nodes / 2 def stable_set_value(x, w): @@ -78,7 +78,7 @@ def stable_set_value(x, w): feasible = True num_nodes = w.shape[0] for i in range(num_nodes): - for j in range(i+1, num_nodes): + for j in range(i + 1, num_nodes): if w[i, j] != 0 and x[i] == 0 and x[j] == 0: feasible = False break diff --git a/qiskit/optimization/ising/tsp.py b/qiskit/optimization/applications/ising/tsp.py similarity index 100% rename from qiskit/optimization/ising/tsp.py rename to qiskit/optimization/applications/ising/tsp.py diff --git a/qiskit/optimization/ising/vehicle_routing.py b/qiskit/optimization/applications/ising/vehicle_routing.py similarity index 96% rename from qiskit/optimization/ising/vehicle_routing.py rename to qiskit/optimization/applications/ising/vehicle_routing.py index 74dbb40aa..3f7b58e96 100644 --- a/qiskit/optimization/ising/vehicle_routing.py +++ b/qiskit/optimization/applications/ising/vehicle_routing.py @@ -2,7 +2,7 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2020 +# (C) Copyright IBM 2019, 2020. # # 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 @@ -46,8 +46,8 @@ def get_vehiclerouting_matrices(instance, n, K): # pylint: disable=invalid-name A = np.max(instance) * 100 # A parameter of cost function # Determine the weights w - instance_vec = instance.reshape(n**2) - w_list = [instance_vec[x] for x in range(n**2) if instance_vec[x] > 0] + instance_vec = instance.reshape(n ** 2) + w_list = [instance_vec[x] for x in range(n ** 2) if instance_vec[x] > 0] w = np.zeros(n * (n - 1)) for i_i, _ in enumerate(w_list): w[i_i] = w_list[i_i] @@ -81,7 +81,7 @@ def get_vehiclerouting_matrices(instance, n, K): # pylint: disable=invalid-name 2 * A * K * (np.kron(neg_iv_n_1, iv_n) + v[0].T) # c is the constant offset - c = 2 * A * (n - 1) + 2 * A * (K**2) + c = 2 * A * (n - 1) + 2 * A * (K ** 2) return (Q, g, c) @@ -103,6 +103,7 @@ def get_vehiclerouting_cost(instance, n, K, x_sol): # pylint: disable=invalid-n def fun(x): return np.dot(np.around(x), np.dot(Q, np.around(x))) + np.dot(g, np.around(x)) + c + cost = fun(x_sol) return cost diff --git a/qiskit/optimization/ising/vertex_cover.py b/qiskit/optimization/applications/ising/vertex_cover.py similarity index 92% rename from qiskit/optimization/ising/vertex_cover.py rename to qiskit/optimization/applications/ising/vertex_cover.py index 436366173..9710300e7 100644 --- a/qiskit/optimization/ising/vertex_cover.py +++ b/qiskit/optimization/applications/ising/vertex_cover.py @@ -62,17 +62,17 @@ def get_operator(weight_matrix): v_p = np.zeros(n) v_p[i] = 1 v_p[j] = 1 - pauli_list.append([a__*0.25, Pauli(v_p, w_p)]) + pauli_list.append([a__ * 0.25, Pauli(v_p, w_p)]) v_p2 = np.zeros(n) v_p2[i] = 1 - pauli_list.append([-a__*0.25, Pauli(v_p2, w_p)]) + pauli_list.append([-a__ * 0.25, Pauli(v_p2, w_p)]) v_p3 = np.zeros(n) v_p3[j] = 1 - pauli_list.append([-a__*0.25, Pauli(v_p3, w_p)]) + pauli_list.append([-a__ * 0.25, Pauli(v_p3, w_p)]) - shift += a__*0.25 + shift += a__ * 0.25 for i in range(n): w_p = np.zeros(n) diff --git a/qiskit/optimization/converters/__init__.py b/qiskit/optimization/converters/__init__.py new file mode 100644 index 000000000..9d41d9c34 --- /dev/null +++ b/qiskit/optimization/converters/__init__.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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 stack for Aqua (:mod:`qiskit.optimization.converters`) +=================================================================== + +.. currentmodule:: qiskit.optimization.converters + +Structures for converting optimization problems +=============================================== + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + InequalityToEquality + IntegerToBinary + QuadraticProgramToNegativeValueOracle + QuadraticProgramToOperator + QuadraticProgramToQubo + LinearEqualityToPenalty + OperatorToQuadraticProgram + +""" + +# no opt problem dependency +from .linear_equality_to_penalty import LinearEqualityToPenalty +from .quadratic_program_to_operator import QuadraticProgramToOperator +from .quadratic_program_to_negative_value_oracle import QuadraticProgramToNegativeValueOracle +from .operator_to_quadratic_program import OperatorToQuadraticProgram + +# opt problem dependency +from .integer_to_binary import IntegerToBinary +from .inequality_to_equality import InequalityToEquality +from .quadratic_program_to_qubo import QuadraticProgramToQubo + +__all__ = [ + "InequalityToEquality", + "IntegerToBinary", + "QuadraticProgramToNegativeValueOracle", + "QuadraticProgramToOperator", + "QuadraticProgramToQubo", + "LinearEqualityToPenalty", + "OperatorToQuadraticProgram" +] diff --git a/qiskit/optimization/converters/inequality_to_equality.py b/qiskit/optimization/converters/inequality_to_equality.py new file mode 100644 index 000000000..48d0064f5 --- /dev/null +++ b/qiskit/optimization/converters/inequality_to_equality.py @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 inequality to equality converter.""" + +import copy +import math +from typing import List, Tuple, Dict, Optional +import logging + +from ..problems.quadratic_program import QuadraticProgram +from ..problems.quadratic_objective import QuadraticObjective +from ..problems.constraint import Constraint +from ..problems.variable import Variable +from ..exceptions import QiskitOptimizationError + +logger = logging.getLogger(__name__) + + +class InequalityToEquality: + """Convert inequality constraints into equality constraints by introducing slack variables. + + Examples: + >>> problem = QuadraticProgram() + >>> # define a problem + >>> conv = InequalityToEquality() + >>> problem2 = conv.encode(problem) + """ + + _delimiter = '@' # users are supposed not to use this character in variable names + + def __init__(self) -> None: + self._src = None + self._dst = None + self._conv: Dict[str, List[Tuple[str, int]]] = {} + # e.g., self._conv = {'c1': [c1@slack_var]} + + def encode( + self, op: QuadraticProgram, name: Optional[str] = None, mode: str = 'auto' + ) -> QuadraticProgram: + """Convert a problem with inequality constraints into one with only equality constraints. + + Args: + op: The problem to be solved, that may contain inequality constraints. + name: The name of the converted problem. + mode: To chose the type of slack variables. There are 3 options for mode. + - 'integer': All slack variables will be integer variables. + - 'continuous': All slack variables will be continuous variables + - 'auto': Try to use integer variables but if it's not possible, + use continuous variables + + Returns: + The converted problem, that contain only equality constraints. + + Raises: + QiskitOptimizationError: If a variable type is not supported. + QiskitOptimizationError: If an unsupported mode is selected. + QiskitOptimizationError: If an unsupported sense is specified. + """ + self._src = copy.deepcopy(op) + self._dst = QuadraticProgram() + if name: + self._dst.name = name + else: + self._dst.name = self._src.name + + # Copy variables + for x in self._src.variables: + if x.vartype == Variable.Type.BINARY: + self._dst.binary_var(name=x.name) + elif x.vartype == Variable.Type.INTEGER: + self._dst.integer_var( + name=x.name, lowerbound=x.lowerbound, upperbound=x.upperbound + ) + elif x.vartype == Variable.Type.CONTINUOUS: + self._dst.continuous_var( + name=x.name, lowerbound=x.lowerbound, upperbound=x.upperbound + ) + else: + raise QiskitOptimizationError("Unsupported variable type {}".format(x.vartype)) + + # Copy the objective function + constant = self._src.objective.constant + linear = self._src.objective.linear.to_dict(use_name=True) + quadratic = self._src.objective.quadratic.to_dict(use_name=True) + if self._src.objective.sense == QuadraticObjective.Sense.MINIMIZE: + self._dst.minimize(constant, linear, quadratic) + else: + self._dst.maximize(constant, linear, quadratic) + + # For linear constraints + for constraint in self._src.linear_constraints: + linear = constraint.linear.to_dict(use_name=True) + if constraint.sense == Constraint.Sense.EQ: + self._dst.linear_constraint( + linear, constraint.sense, constraint.rhs, constraint.name + ) + elif constraint.sense == Constraint.Sense.LE or constraint.sense == Constraint.Sense.GE: + if mode == 'integer': + self._add_integer_slack_var_linear_constraint( + linear, constraint.sense, constraint.rhs, constraint.name + ) + elif mode == 'continuous': + self._add_continuous_slack_var_linear_constraint( + linear, constraint.sense, constraint.rhs, constraint.name + ) + elif mode == 'auto': + self._add_auto_slack_var_linear_constraint( + linear, constraint.sense, constraint.rhs, constraint.name + ) + else: + raise QiskitOptimizationError('Unsupported mode is selected: {}'.format(mode)) + else: + raise QiskitOptimizationError( + 'Type of sense in {} is not supported'.format(constraint.name) + ) + + # For quadratic constraints + for constraint in self._src.quadratic_constraints: + linear = constraint.linear.to_dict(use_name=True) + quadratic = constraint.quadratic.to_dict(use_name=True) + if constraint.sense == Constraint.Sense.EQ: + self._dst.quadratic_constraint( + linear, quadratic, constraint.sense, constraint.rhs, constraint.name + ) + elif constraint.sense == Constraint.Sense.LE or constraint.sense == Constraint.Sense.GE: + if mode == 'integer': + self._add_integer_slack_var_quadratic_constraint( + linear, + quadratic, + constraint.sense, + constraint.rhs, + constraint.name, + ) + elif mode == 'continuous': + self._add_continuous_slack_var_quadratic_constraint( + linear, + quadratic, + constraint.sense, + constraint.rhs, + constraint.name, + ) + elif mode == 'auto': + self._add_auto_slack_var_quadratic_constraint( + linear, + quadratic, + constraint.sense, + constraint.rhs, + constraint.name, + ) + else: + raise QiskitOptimizationError('Unsupported mode is selected: {}'.format(mode)) + else: + raise QiskitOptimizationError( + 'Type of sense in {} is not supported'.format(constraint.name) + ) + + return self._dst + + def _add_integer_slack_var_linear_constraint(self, linear, sense, rhs, name): + # If a coefficient that is not integer exist, raise error + if any(isinstance(coef, float) and not coef.is_integer() for coef in linear.values()): + raise QiskitOptimizationError('Can not use a slack variable for ' + name) + + # If rhs is float number, round up/down to the nearest integer. + new_rhs = rhs + if sense == Constraint.Sense.LE: + new_rhs = math.floor(rhs) + if sense == Constraint.Sense.GE: + new_rhs = math.ceil(rhs) + + # Add a new integer variable. + slack_name = name + self._delimiter + 'int_slack' + self._conv[name] = slack_name + + lhs_lb, lhs_ub = self._calc_linear_bounds(linear) + + if sense == Constraint.Sense.LE: + sign = 1 + self._dst.integer_var(name=slack_name, lowerbound=0, upperbound=new_rhs - lhs_lb) + elif sense == Constraint.Sense.GE: + sign = -1 + self._dst.integer_var(name=slack_name, lowerbound=0, upperbound=lhs_ub - new_rhs) + else: + raise QiskitOptimizationError('The type of Sense in {} is not supported'.format(name)) + + # Add a new equality constraint. + new_linear = copy.deepcopy(linear) + new_linear[slack_name] = sign + self._dst.linear_constraint(new_linear, "==", new_rhs, name) + + def _add_continuous_slack_var_linear_constraint(self, linear, sense, rhs, name): + slack_name = name + self._delimiter + 'continuous_slack' + self._conv[name] = slack_name + + lhs_lb, lhs_ub = self._calc_linear_bounds(linear) + + if sense == Constraint.Sense.LE: + sign = 1 + self._dst.continuous_var(name=slack_name, lowerbound=0, upperbound=rhs - lhs_lb) + elif sense == Constraint.Sense.GE: + sign = -1 + self._dst.continuous_var(name=slack_name, lowerbound=0, upperbound=lhs_ub - rhs) + else: + raise QiskitOptimizationError('The type of Sense in {} is not supported'.format(name)) + + # Add a new equality constraint. + new_linear = copy.deepcopy(linear) + new_linear[slack_name] = sign + self._dst.linear_constraint(new_linear, "==", rhs, name) + + def _add_auto_slack_var_linear_constraint(self, linear, sense, rhs, name): + # If a coefficient that is not integer exist, use a continuous slack variable + if any(isinstance(coef, float) and not coef.is_integer() for coef in linear.values()): + self._add_continuous_slack_var_linear_constraint(linear, sense, rhs, name) + # Else use an integer slack variable + else: + self._add_integer_slack_var_linear_constraint(linear, sense, rhs, name) + + def _add_integer_slack_var_quadratic_constraint(self, linear, quadratic, sense, rhs, name): + # If a coefficient that is not integer exist, raise an error + if any( + isinstance(coef, float) and not coef.is_integer() for coef in quadratic.values() + ) or any(isinstance(coef, float) and not coef.is_integer() for coef in linear.values()): + raise QiskitOptimizationError('Can not use a slack variable for ' + name) + + # If rhs is float number, round up/down to the nearest integer. + new_rhs = rhs + if sense == Constraint.Sense.LE: + new_rhs = math.floor(rhs) + if sense == Constraint.Sense.GE: + new_rhs = math.ceil(rhs) + + # Add a new integer variable. + slack_name = name + self._delimiter + 'int_slack' + self._conv[name] = slack_name + + lhs_lb, lhs_ub = self._calc_quadratic_bounds(linear, quadratic) + + if sense == Constraint.Sense.LE: + sign = 1 + self._dst.integer_var(name=slack_name, lowerbound=0, upperbound=new_rhs - lhs_lb) + elif sense == Constraint.Sense.GE: + sign = -1 + self._dst.integer_var(name=slack_name, lowerbound=0, upperbound=lhs_ub - new_rhs) + else: + raise QiskitOptimizationError('The type of Sense in {} is not supported'.format(name)) + + # Add a new equality constraint. + new_linear = copy.deepcopy(linear) + new_linear[slack_name] = sign + self._dst.quadratic_constraint(new_linear, quadratic, "==", new_rhs, name) + + def _add_continuous_slack_var_quadratic_constraint(self, linear, quadratic, sense, rhs, name): + # If a coefficient that is not integer exist, raise error + if any( + isinstance(coef, float) and not coef.is_integer() for coef in quadratic.values() + ) or any(isinstance(coef, float) and not coef.is_integer() for coef in linear.values()): + raise QiskitOptimizationError('Can not use a slack variable for ' + name) + + # Add a new continuous variable. + slack_name = name + self._delimiter + 'continuous_slack' + self._conv[name] = slack_name + + lhs_lb, lhs_ub = self._calc_quadratic_bounds(linear, quadratic) + + if sense == Constraint.Sense.LE: + sign = 1 + self._dst.continuous_var(name=slack_name, lowerbound=0, upperbound=rhs - lhs_lb) + elif sense == Constraint.Sense.GE: + sign = -1 + self._dst.continuous_var(name=slack_name, lowerbound=0, upperbound=lhs_ub - rhs) + else: + raise QiskitOptimizationError('The type of Sense in {} is not supported'.format(name)) + + # Add a new equality constraint. + new_linear = copy.deepcopy(linear) + new_linear[slack_name] = sign + self._dst.quadratic_constraint(new_linear, quadratic, "==", rhs, name) + + def _add_auto_slack_var_quadratic_constraint(self, linear, quadratic, sense, rhs, name): + # If a coefficient that is not integer exist, use a continuous slack variable + if any( + isinstance(coef, float) and not coef.is_integer() for coef in quadratic.values() + ) or any(isinstance(coef, float) and not coef.is_integer() for coef in linear.values()): + self._add_continuous_slack_var_quadratic_constraint(linear, quadratic, sense, rhs, name) + # Else use an integer slack variable + else: + self._add_integer_slack_var_quadratic_constraint(linear, quadratic, sense, rhs, name) + + def _calc_linear_bounds(self, linear): + lhs_lb, lhs_ub = 0, 0 + for var_name, v in linear.items(): + x = self._src.get_variable(var_name) + lhs_lb += min(x.lowerbound * v, x.upperbound * v) + lhs_ub += max(x.lowerbound * v, x.upperbound * v) + return lhs_lb, lhs_ub + + def _calc_quadratic_bounds(self, linear, quadratic): + lhs_lb, lhs_ub = 0, 0 + # Calculate the lowerbound and the upperbound of the linear part + linear_lb, linear_ub = self._calc_linear_bounds(linear) + lhs_lb += linear_lb + lhs_ub += linear_ub + + # Calculate the lowerbound and the upperbound of the quadratic part + for (name_i, name_j), v in quadratic.items(): + x = self._src.get_variable(name_i) + y = self._src.get_variable(name_j) + + lhs_lb += min( + x.lowerbound * y.lowerbound * v, + x.lowerbound * y.upperbound * v, + x.upperbound * y.lowerbound * v, + x.upperbound * y.upperbound * v, + ) + lhs_ub += max( + x.lowerbound * y.lowerbound * v, + x.lowerbound * y.upperbound * v, + x.upperbound * y.lowerbound * v, + x.upperbound * y.upperbound * v, + ) + return lhs_lb, lhs_ub + + def decode(self, result: 'OptimizationResult') -> 'OptimizationResult': + """Convert a result of a converted problem into that of the original problem. + + Args: + result: The result of the converted problem. + + Returns: + The result of the original problem. + """ + + # convert the optimization result into that of the original problem + names = [x.name for x in self._dst.variables] + vals = result.x + new_vals = self._decode_var(names, vals) + result.x = new_vals + return result + + def _decode_var(self, names, vals) -> List[int]: + # decode slack variables + sol = {name: vals[i] for i, name in enumerate(names)} + + new_vals = [] + for x in self._src.variables: + new_vals.append(sol[x.name]) + return new_vals diff --git a/qiskit/optimization/converters/integer_to_binary.py b/qiskit/optimization/converters/integer_to_binary.py new file mode 100644 index 000000000..f09051b14 --- /dev/null +++ b/qiskit/optimization/converters/integer_to_binary.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 converter to map integer variables in a quadratic program to binary variables.""" + +import copy +import logging +from typing import Dict, List, Optional, Tuple + +import numpy as np + +from ..exceptions import QiskitOptimizationError +from ..problems.quadratic_objective import QuadraticObjective +from ..problems.quadratic_program import QuadraticProgram +from ..problems.variable import Variable + +logger = logging.getLogger(__name__) + + +class IntegerToBinary: + """Convert an `QuadraticProgram` into new one by encoding integer with binary variables. + + This bounded-coefficient encoding used in this converted is proposed in [1], Eq. (5). + + Examples: + >>> problem = QuadraticProgram() + >>> problem.integer_var(name='x', lowerbound=0, upperbound=10) + >>> conv = IntegerToBinary() + >>> problem2 = conv.encode(problem) + + References: + [1]: Sahar Karimi, Pooya Ronagh (2017), Practical Integer-to-Binary Mapping for Quantum + Annealers. arxiv.org:1706.01945. + """ + + _delimiter = '@' # users are supposed not to use this character in variable names + + def __init__(self) -> None: + self._src = None + self._dst = None + self._conv: Dict[Variable, List[Tuple[str, int]]] = {} + # e.g., self._conv = {x: [('x@1', 1), ('x@2', 2)]} + + def encode(self, op: QuadraticProgram, name: Optional[str] = None) -> QuadraticProgram: + """Convert an integer problem into a new problem with binary variables. + + Args: + op: The problem to be solved, that may contain integer variables. + name: The name of the converted problem. If not provided, the name of the input + problem is used. + + Returns: + The converted problem, that contains no integer variables. + + Raises: + QiskitOptimizationError: if variable or constraint type is not supported. + """ + + # copy original QP as reference. + self._src = copy.deepcopy(op) + + if self._src.get_num_integer_vars() > 0: + + # initialize new QP + self._dst = QuadraticProgram() + + # declare variables + for x in self._src.variables: + if x.vartype == Variable.Type.INTEGER: + new_vars = self._encode_var(x.name, x.lowerbound, x.upperbound) + self._conv[x] = new_vars + for (var_name, _) in new_vars: + self._dst.binary_var(var_name) + else: + if x.vartype == Variable.Type.CONTINUOUS: + self._dst.continuous_var(x.lowerbound, x.upperbound, x.name) + elif x.vartype == Variable.Type.BINARY: + self._dst.binary_var(x.name) + else: + raise QiskitOptimizationError( + "Unsupported variable type {}".format(x.vartype)) + + self._substitute_int_var() + + else: + # just copy the problem if no integer variables exist + self._dst = copy.deepcopy(op) + + # adjust name of resulting problem if necessary + if name: + self._dst.name = name + else: + self._dst.name = self._src.name + + return self._dst + + def _encode_var(self, name: str, lowerbound: int, upperbound: int) -> List[Tuple[str, int]]: + var_range = upperbound - lowerbound + power = int(np.log2(var_range)) + bounded_coef = var_range - (2 ** power - 1) + + coeffs = [2 ** i for i in range(power)] + [bounded_coef] + return [(name + self._delimiter + str(i), coef) for i, coef in enumerate(coeffs)] + + def _encode_linear_coefficients_dict(self, coefficients: Dict[str, float]) \ + -> Tuple[Dict[str, float], float]: + constant = 0 + linear = {} + for name, v in coefficients.items(): + x = self._src.get_variable(name) + if x in self._conv: + for y, coeff in self._conv[x]: + linear[y] = v * coeff + constant += v * x.lowerbound + else: + linear[x.name] = v + + return linear, constant + + def _encode_quadratic_coefficients_dict(self, coefficients: Dict[Tuple[str, str], float]) \ + -> Tuple[Dict[Tuple[str, str], float], Dict[str, float], float]: + constant = 0 + linear = {} + quadratic = {} + for (name_i, name_j), v in coefficients.items(): + x = self._src.get_variable(name_i) + y = self._src.get_variable(name_j) + + if x in self._conv and y not in self._conv: + for z_x, coeff_x in self._conv[x]: + quadratic[z_x, y.name] = v * coeff_x + linear[y.name] = linear.get(y.name, 0.0) + v * x.lowerbound + + elif x not in self._conv and y in self._conv: + for z_y, coeff_y in self._conv[y]: + quadratic[x.name, z_y] = v * coeff_y + linear[x.name] = linear.get(x.name, 0.0) + v * y.lowerbound + + elif x in self._conv and y in self._conv: + for z_x, coeff_x in self._conv[x]: + for z_y, coeff_y in self._conv[y]: + quadratic[z_x, z_y] = v * coeff_x * coeff_y + + for z_x, coeff_x in self._conv[x]: + linear[z_x] = linear.get(z_x, 0.0) + v * y.lowerbound + for z_y, coeff_y in self._conv[y]: + linear[z_y] = linear.get(z_y, 0.0) + v * x.lowerbound + + constant += v * x.lowerbound * y.lowerbound + + else: + quadratic[x.name, y.name] = v + + return quadratic, linear, constant + + def _substitute_int_var(self): + + # set objective + linear, linear_constant = self._encode_linear_coefficients_dict( + self._src.objective.linear.to_dict(use_name=True)) + quadratic, quadratic_linear, quadratic_constant = \ + self._encode_quadratic_coefficients_dict( + self._src.objective.quadratic.to_dict(use_name=True)) + + constant = self._src.objective.constant + linear_constant + quadratic_constant + for i, v in quadratic_linear.items(): + linear[i] = linear.get(i, 0) + v + + if self._src.objective.sense == QuadraticObjective.Sense.MINIMIZE: + self._dst.minimize(constant, linear, quadratic) + else: + self._dst.maximize(constant, linear, quadratic) + + # set linear constraints + for constraint in self._src.linear_constraints: + linear, constant = self._encode_linear_coefficients_dict(constraint.linear.to_dict()) + self._dst.linear_constraint(linear, constraint.sense, + constraint.rhs - constant, constraint.name) + + # set quadratic constraints + for constraint in self._src.quadratic_constraints: + linear, linear_constant = self._encode_linear_coefficients_dict( + constraint.linear.to_dict()) + quadratic, quadratic_linear, quadratic_constant = \ + self._encode_quadratic_coefficients_dict(constraint.quadratic.to_dict()) + + constant = linear_constant + quadratic_constant + for i, v in quadratic_linear.items(): + linear[i] = linear.get(i, 0) + v + + self._dst.quadratic_constraint(linear, quadratic, constraint.sense, + constraint.rhs - constant, constraint.name) + + def decode(self, result: "OptimizationResult") -> "OptimizationResult": + """Convert the encoded problem (binary variables) back to the original (integer variables). + + Args: + result: The result of the converted problem. + + Returns: + The result of the original problem. + """ + vals = result.x + new_vals = self._decode_var(vals) + result.x = new_vals + return result + + def _decode_var(self, vals) -> List[int]: + # decode integer values + sol = {x.name: float(vals[i]) for i, x in enumerate(self._dst.variables)} + new_vals = [] + for x in self._src.variables: + if x in self._conv: + new_vals.append(sum(sol[aux] * coef for aux, coef in self._conv[x]) + x.lowerbound) + else: + new_vals.append(sol[x.name]) + return new_vals diff --git a/qiskit/optimization/converters/linear_equality_to_penalty.py b/qiskit/optimization/converters/linear_equality_to_penalty.py new file mode 100644 index 000000000..e87f84a66 --- /dev/null +++ b/qiskit/optimization/converters/linear_equality_to_penalty.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""Converter to convert a problem with equality constraints to unconstrained with penalty terms.""" + +import copy +from typing import Optional + +from ..problems import QuadraticProgram, Variable, Constraint, QuadraticObjective +from ..exceptions import QiskitOptimizationError + + +class LinearEqualityToPenalty: + """Convert a problem with only equality constraints to unconstrained with penalty terms.""" + + def __init__(self): + self._src = None + self._dst = None + + def encode(self, op: QuadraticProgram, penalty_factor: float = 1e5, + name: Optional[str] = None) -> QuadraticProgram: + """Convert a problem with equality constraints into an unconstrained problem. + + Args: + op: The problem to be solved, that does not contain inequality constraints. + penalty_factor: Penalty terms in the objective function is multiplied with this factor. + name: The name of the converted problem. + + Returns: + The converted problem, that is an unconstrained problem. + + Raises: + QiskitOptimizationError: If an inequality constraint exists. + """ + + # create empty QuadraticProgram model + self._src = copy.deepcopy(op) # deep copy + self._dst = QuadraticProgram() + + # set variables + for x in self._src.variables: + if x.vartype == Variable.Type.CONTINUOUS: + self._dst.continuous_var(x.lowerbound, x.upperbound, x.name) + elif x.vartype == Variable.Type.BINARY: + self._dst.binary_var(x.name) + elif x.vartype == Variable.Type.INTEGER: + self._dst.integer_var(x.lowerbound, x.upperbound, x.name) + else: + raise QiskitOptimizationError('Unsupported vartype: {}'.format(x.vartype)) + + # set problem name + if name is None: + self._dst.name = self._src.name + else: + self._dst.name = name + + # get original objective terms + offset = self._src.objective.constant + linear = self._src.objective.linear.to_dict() + quadratic = self._src.objective.quadratic.to_dict() + sense = self._src.objective.sense.value + + # convert linear constraints into penalty terms + for constraint in self._src.linear_constraints: + + if constraint.sense != Constraint.Sense.EQ: + raise QiskitOptimizationError('An inequality constraint exists. ' + 'The method supports only equality constraints.') + + constant = constraint.rhs + row = constraint.linear.to_dict() + + # constant parts of penalty*(Constant-func)**2: penalty*(Constant**2) + offset += sense * penalty_factor * constant**2 + + # linear parts of penalty*(Constant-func)**2: penalty*(-2*Constant*func) + for j, coef in row.items(): + # if j already exists in the linear terms dic, add a penalty term + # into existing value else create new key and value in the linear_term dict + linear[j] = linear.get(j, 0.0) + sense * penalty_factor * -2 * coef * constant + + # quadratic parts of penalty*(Constant-func)**2: penalty*(func**2) + for j, coef_1 in row.items(): + for k, coef_2 in row.items(): + # if j and k already exist in the quadratic terms dict, + # add a penalty term into existing value + # else create new key and value in the quadratic term dict + + # according to implementation of quadratic terms in OptimizationModel, + # don't need to multiply by 2, since loops run over (x, y) and (y, x). + quadratic[(j, k)] = quadratic.get((j, k), 0.0) \ + + sense * penalty_factor * coef_1 * coef_2 + + if self._src.objective.sense == QuadraticObjective.Sense.MINIMIZE: + self._dst.minimize(offset, linear, quadratic) + else: + self._dst.maximize(offset, linear, quadratic) + + return self._dst diff --git a/qiskit/optimization/converters/operator_to_quadratic_program.py b/qiskit/optimization/converters/operator_to_quadratic_program.py new file mode 100644 index 000000000..e808ff6cc --- /dev/null +++ b/qiskit/optimization/converters/operator_to_quadratic_program.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 converter from a ```Operator``` to ``QuadraticProgram``.""" + +import copy + +import numpy as np + +from qiskit.aqua.operators import WeightedPauliOperator +from ..problems.quadratic_program import QuadraticProgram +from ..exceptions import QiskitOptimizationError + + +class OperatorToQuadraticProgram: + """Convert a qubit operator into a quadratic program""" + + def __init__(self) -> None: + """Initialize the internal data structure.""" + self._qubit_op = None + self._offset = 0 + self._num_qubits = 0 + self._qubo_matrix = None + self._qp = None + + def encode(self, qubit_op: WeightedPauliOperator, offset: float = 0.0) -> QuadraticProgram: + """Convert a qubit operator and a shift value into a quadratic program + + Args: + qubit_op: The qubit operator to be converted into `QuadraticProgram` + offset: The shift value of the qubit operator + + Returns: + QuadraticProgram: converted from the input qubit operator and the shift value + + Raises: + QiskitOptimizationError: If there are Pauli Xs in any Pauli term + QiskitOptimizationError: If there are more than 2 Pauli Zs in any Pauli term + """ + # Set properties + self._qubit_op = qubit_op + self._offset = copy.deepcopy(offset) + self._num_qubits = qubit_op.num_qubits + + # Create `QuadraticProgram` + self._qp = QuadraticProgram() + for i in range(self._num_qubits): + self._qp.binary_var(name='x_{0}'.format(i)) + # Create QUBO matrix + self._create_qubo_matrix() + + # Initialize dicts for linear terms and quadratic terms + linear = {} + quadratic = {} + + # For quadratic pauli terms of operator + # x_i * x_ j = (1 - Z_i - Z_j + Z_i * Z_j)/4 + for i, row in enumerate(self._qubo_matrix): + for j, weight in enumerate(row): + # Focus on the upper triangular matrix + if j <= i: + continue + # Add a quadratic term to the object function of `QuadraticProgram` + # The coefficient of the quadratic term in `QuadraticProgram` is + # 4 * weight of the pauli + coef = weight * 4 + quadratic[(i, j)] = coef + # Sub the weight of the quadratic pauli term from the QUBO matrix + self._qubo_matrix[i, j] -= weight + # Sub the weight of the linear pauli term from the QUBO matrix + self._qubo_matrix[i, i] += weight + self._qubo_matrix[j, j] += weight + # Sub the weight from offset + offset -= weight + + # After processing quadratic pauli terms, only linear paulis are left + # x_i = (1 - Z_i)/2 + for i in range(self._num_qubits): + weight = self._qubo_matrix[i, i] + # Add a linear term to the object function of `QuadraticProgram` + # The coefficient of the linear term in `QuadraticProgram` is + # 2 * weight of the pauli + coef = weight * 2 + linear[i] = -coef + # Sub the weight of the linear pauli term from the QUBO matrix + self._qubo_matrix[i, i] -= weight + offset += weight + + self._qp.minimize(offset, linear, quadratic) + offset -= offset + + return self._qp + + def _create_qubo_matrix(self): + """Create a QUBO matrix from the qubit operator + + Raises: + QiskitOptimizationError: If there are Pauli Xs in any Pauli term + QiskitOptimizationError: If there are more than 2 Pauli Zs in any Pauli term + + """ + # Set properties + # The Qubo matrix is an upper triangular matrix. + # Diagonal elements in the QUBO matrix is for linear terms of the qubit operator + # The other elements in the QUBO matrix is for quadratic terms of the qubit operator + self._qubo_matrix = np.zeros((self._num_qubits, self._num_qubits)) + + for pauli in self._qubit_op.paulis: + # Count the number of Pauli Zs in a Pauli term + lst_z = pauli[1].z.tolist() + z_index = [i for i, z in enumerate(lst_z) if z is True] + num_z = len(z_index) + + # Add its weight of the Pauli term to the corresponding element of QUBO matrix + if num_z == 1: + self._qubo_matrix[z_index[0], z_index[0]] = pauli[0].real + elif num_z == 2: + self._qubo_matrix[z_index[0], z_index[1]] = pauli[0].real + else: + raise QiskitOptimizationError( + 'There are more than 2 Pauli Zs in the Pauli term {}'.format(pauli[1].z) + ) + + # If there are Pauli Xs in the Pauli term, raise an error + lst_x = pauli[1].x.tolist() + x_index = [i for i, x in enumerate(lst_x) if x is True] + if len(x_index) > 0: + raise QiskitOptimizationError('Pauli Xs exist in the Pauli {}'.format(pauli[1].x)) diff --git a/qiskit/optimization/converters/quadratic_program_to_negative_value_oracle.py b/qiskit/optimization/converters/quadratic_program_to_negative_value_oracle.py new file mode 100644 index 000000000..0d4ade34f --- /dev/null +++ b/qiskit/optimization/converters/quadratic_program_to_negative_value_oracle.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""QuadraticProgramToNegativeValueOracle module""" + +import logging +from typing import Tuple, Dict, Union + +import numpy as np + +from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister +from qiskit.circuit.library import QFT +from qiskit.aqua.components.initial_states import Custom +from qiskit.aqua.components.oracles import CustomCircuitOracle +from qiskit.optimization.problems import QuadraticProgram + +logger = logging.getLogger(__name__) + + +class QuadraticProgramToNegativeValueOracle: + """Converts an optimization problem (QUBO) to a negative value oracle. + + In addition, a state preparation operator is generated from the coefficients and constant of a + QUBO, which can be used to encode the function into a quantum state. In conjunction, this oracle + and operator can be used to flag the negative values of a QUBO encoded in a quantum state. + + The construction of the oracle is discussed in [1]. + + References: + [1]: Gilliam et al., Grover Adaptive Search for Constrained Polynomial Binary Optimization. + arxiv:1912.04088. + """ + + def __init__(self, num_output_qubits: int, measurement: bool = False) -> None: + """ + Args: + num_output_qubits: The number of qubits required to represent the output. + measurement: Whether the A operator contains measurements. + """ + self._num_key = 0 + self._num_value = num_output_qubits + self._measurement = measurement + + def encode(self, problem: QuadraticProgram) -> \ + Tuple[Custom, CustomCircuitOracle, Dict[Union[int, Tuple[int, int]], int]]: + """A helper function that converts a QUBO into an oracle that recognizes negative numbers. + + Args: + problem: The problem to be solved. + + Returns: + A state preparation operator A, an oracle O that recognizes negative numbers, and + a dictionary representation of the function coefficients, where the key -1 represents + the constant. + """ + + # get linear part of objective + linear_dict = problem.objective.linear.to_dict() + linear_coeff = np.zeros(len(problem.variables)) + for i, v in linear_dict.items(): + linear_coeff[i] = v + + # get quadratic part of objective + quadratic_coeff = problem.objective.quadratic.to_dict() + + constant = int(problem.objective.constant) + + # Get circuit requirements from input. + self._num_key = len(linear_coeff) + + # Get the function dictionary. + func = self._get_function(linear_coeff, quadratic_coeff, constant) + logger.info("Function: %s\n", func) + + # Define state preparation operator A from function. + a_operator_circuit = self._build_operator(func) + a_operator = Custom(a_operator_circuit.width(), circuit=a_operator_circuit) + + # Get registers from the A operator circuit. + reg_map = {} + for reg in a_operator_circuit.qregs: + reg_map[reg.name] = reg + key_val = reg_map["key_value"] + + # Build negative value oracle O. + oracle_bit = QuantumRegister(1, "oracle") + oracle_circuit = QuantumCircuit(key_val, oracle_bit) + oracle_circuit.z(key_val[self._num_key]) # recognize negative values. + oracle = CustomCircuitOracle(variable_register=key_val, + output_register=oracle_bit, + circuit=oracle_circuit, + evaluate_classically_callback=self._evaluate_classically) + + return a_operator, oracle, func + + @staticmethod + def _get_function(linear: np.array, quadratic: np.array, constant: int) -> \ + Dict[Union[int, Tuple[int, int]], int]: + """Convert the problem to a dictionary format.""" + func = {-1: int(constant)} + for i, v in enumerate(linear): + func[i] = int(v) + for (i, j), v in quadratic.items(): + if i != j: + func[(i, j)] = int(quadratic[(i, j)]) + else: + func[i] += int(v) + + return func + + def _evaluate_classically(self, measurement): + """ evaluate classical """ + value = measurement[self._num_key:self._num_key + self._num_value] + assignment = [(var + 1) * (int(tf) * 2 - 1) for tf, var in zip(measurement, + range(len(measurement)))] + evaluation = value[0] == '1' + return evaluation, assignment + + def _build_operator(self, func_dict: Dict[Union[int, Tuple[int, int]], int]) -> QuantumCircuit: + """Creates a circuit for the state preparation operator. + + Args: + func_dict: Representation of the QUBO problem. The keys should be subscripts of the + coefficients (e.g. x_1 -> 1), with the constant (if present) being represented with + a key of -1 (i.e. d[-1] = constant). Quadratic coefficients should use a tuple for + the key, with the corresponding subscripts inside (e.g. 2*x_1*x_2 -> d[(1,2)]=2). + + Returns: + Circuit object describing the state preparation operator. + """ + + # Build initial circuit. + key_val = QuantumRegister(self._num_key + self._num_value, "key_value") + circuit = QuantumCircuit(key_val) + if self._measurement: + measure = ClassicalRegister(self._num_key + self._num_value) + circuit.add_register(measure) + circuit.h(key_val) + + # Linear Coefficients. + for i in range(self._num_value): + if func_dict.get(-1, 0) != 0: + circuit.u1(1 / 2 ** self._num_value * 2 * np.pi * 2 ** i * func_dict[-1], + key_val[self._num_key + i]) + for j in range(self._num_key): + if func_dict.get(j, 0) != 0: + circuit.cu1(1 / 2 ** self._num_value * 2 * np.pi * 2 ** i * func_dict[j], + key_val[j], key_val[self._num_key + i]) + + # Quadratic Coefficients. + for i in range(self._num_value): + for k, v in func_dict.items(): + if isinstance(k, tuple): + a_v = [key_val[int(k[0])], key_val[int(k[1])]] + b_v = key_val[self._num_key + i] + circuit.mcu1(1 / 2 ** self._num_value * 2 * np.pi * 2 ** i * v, + a_v, b_v) + + # Add IQFT. Adding swaps at the end of the IQFT, not the beginning. + iqft = QFT(self._num_value, do_swaps=False).inverse() + value = [key_val[v] for v in range(self._num_key, self._num_key + self._num_value)] + circuit.compose(iqft, qubits=value, inplace=True) + + for i in range(len(value) // 2): + circuit.swap(value[i], value[-(i + 1)]) + + return circuit diff --git a/qiskit/optimization/converters/quadratic_program_to_operator.py b/qiskit/optimization/converters/quadratic_program_to_operator.py new file mode 100644 index 000000000..693d1699b --- /dev/null +++ b/qiskit/optimization/converters/quadratic_program_to_operator.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 converter from an ```QuadraticProgram``` to ``Operator``.""" + +from typing import Tuple + +import numpy as np +from qiskit.quantum_info import Pauli + +from qiskit.aqua.operators import WeightedPauliOperator + +from ..problems.quadratic_program import QuadraticProgram +from ..exceptions import QiskitOptimizationError + + +class QuadraticProgramToOperator: + """Convert an optimization problem into a qubit operator.""" + + def __init__(self) -> None: + """Initialize the internal data structure.""" + self._src = None + + def encode(self, op: QuadraticProgram) -> Tuple[WeightedPauliOperator, float]: + """Convert a problem into a qubit operator + + Args: + op: The optimization problem to be converted. Must be an unconstrained problem with + binary variables only. + + Returns: + The qubit operator of the problem and the shift value. + + Raises: + QiskitOptimizationError: If a variable type is not binary. + QiskitOptimizationError: If constraints exist in the problem. + """ + + self._src = op + # if op has variables that are not binary, raise an error + if self._src.get_num_vars() > self._src.get_num_binary_vars(): + raise QiskitOptimizationError('The type of variable must be a binary variable.') + + # if constraints exist, raise an error + if self._src.linear_constraints \ + or self._src.quadratic_constraints: + raise QiskitOptimizationError('An constraint exists. ' + 'The method supports only model with no constraints.') + + # initialize Hamiltonian. + num_nodes = self._src.get_num_vars() + pauli_list = [] + shift = 0 + zero = np.zeros(num_nodes, dtype=np.bool) + + # set a sign corresponding to a maximized or minimized problem. + # sign == 1 is for minimized problem. sign == -1 is for maximized problem. + sense = self._src.objective.sense.value + + # convert a constant part of the object function into Hamiltonian. + shift += self._src.objective.constant * sense + + # convert linear parts of the object function into Hamiltonian. + for i, coef in self._src.objective.linear.to_dict().items(): + z_p = np.zeros(num_nodes, dtype=np.bool) + weight = coef * sense / 2 + z_p[i] = True + + pauli_list.append([-weight, Pauli(z_p, zero)]) + shift += weight + + # convert quadratic parts of the object function into Hamiltonian. + # first merge coefficients (i, j) and (j, i) + coeffs = {} + for (i, j), coeff in self._src.objective.quadratic.to_dict().items(): + if j < i: + coeffs[(j, i)] = coeffs.get((j, i), 0.0) + coeff + else: + coeffs[(i, j)] = coeffs.get((i, j), 0.0) + coeff + + # create Pauli terms + for (i, j), coeff in coeffs.items(): + + weight = coeff * sense / 4 + + if i == j: + shift += weight + else: + z_p = np.zeros(num_nodes, dtype=np.bool) + z_p[i] = True + z_p[j] = True + pauli_list.append([weight, Pauli(z_p, zero)]) + + z_p = np.zeros(num_nodes, dtype=np.bool) + z_p[i] = True + pauli_list.append([-weight, Pauli(z_p, zero)]) + + z_p = np.zeros(num_nodes, dtype=np.bool) + z_p[j] = True + pauli_list.append([-weight, Pauli(z_p, zero)]) + + shift += weight + + # Remove paulis whose coefficients are zeros. + qubit_op = WeightedPauliOperator(paulis=pauli_list) + + return qubit_op, shift diff --git a/qiskit/optimization/converters/quadratic_program_to_qubo.py b/qiskit/optimization/converters/quadratic_program_to_qubo.py new file mode 100644 index 000000000..5e9e15b04 --- /dev/null +++ b/qiskit/optimization/converters/quadratic_program_to_qubo.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""A converter from quadratic program to a QUBO.""" + +from typing import Optional + +from qiskit.optimization.problems import QuadraticProgram +from qiskit.optimization.problems.constraint import Constraint +from qiskit.optimization.converters.linear_equality_to_penalty import LinearEqualityToPenalty +from qiskit.optimization.converters.integer_to_binary import IntegerToBinary +from qiskit.optimization.exceptions import QiskitOptimizationError + + +class QuadraticProgramToQubo: + """Convert a given optimization problem to a new problem that is a QUBO. + + Examples: + >>> problem = QuadraticProgram() + >>> # define a problem + >>> conv = QuadraticProgramToQubo() + >>> problem2 = conv.encode(problem) + """ + + def __init__(self, penalty: Optional[float] = None) -> None: + """ + Args: + penalty: Penalty factor to scale equality constraints that are added to objective. + """ + self._int_to_bin = IntegerToBinary() + self._penalize_lin_eq_constraints = LinearEqualityToPenalty() + self._penalty = penalty + + def encode(self, problem: QuadraticProgram) -> QuadraticProgram: + """Convert a problem with linear equality constraints into new one with a QUBO form. + + Args: + problem: The problem with linear equality constraints to be solved. + + Returns: + The problem converted in QUBO format. + + Raises: + QiskitOptimizationError: In case of an incompatible problem. + """ + + # analyze compatibility of problem + msg = self.get_compatibility_msg(problem) + if len(msg) > 0: + raise QiskitOptimizationError('Incompatible problem: {}'.format(msg)) + + # map integer variables to binary variables + problem_ = self._int_to_bin.encode(problem) + + # penalize linear equality constraints with only binary variables + if self._penalty is None: + # TODO: should be derived from problem + penalty = 1e5 + else: + penalty = self._penalty + problem_ = self._penalize_lin_eq_constraints.encode(problem_, penalty_factor=penalty) + + # return QUBO + return problem_ + + def decode(self, result: 'OptimizationResult') -> 'OptimizationResult': + """ Convert a result of a converted problem into that of the original problem. + + Args: + result: The result of the converted problem. + + Returns: + The result of the original problem. + """ + return self._int_to_bin.decode(result) + + @staticmethod + def get_compatibility_msg(problem: QuadraticProgram) -> str: + """Checks whether a given problem can be solved with this optimizer. + + Checks whether the given problem is compatible, i.e., whether the problem can be converted + to a QUBO, and otherwise, returns a message explaining the incompatibility. + + Args: + problem: The optimization problem to check compatibility. + + Returns: + A message describing the incompatibility. + """ + + # initialize message + msg = '' + + # check whether there are incompatible variable types + if problem.get_num_continuous_vars() > 0: + msg += 'Continuous variables are not supported! ' + + # check whether there are incompatible constraint types + if not all([constraint.sense == Constraint.Sense.EQ + for constraint in problem.linear_constraints]): + msg += 'Only linear equality constraints are supported.' + if len(problem.quadratic_constraints) > 0: + msg += 'Quadratic constraints are not supported. ' + + # if an error occurred, return error message, otherwise, return None + return msg + + def is_compatible(self, problem: QuadraticProgram) -> bool: + """Checks whether a given problem can be solved with the optimizer implementing this method. + + Args: + problem: The optimization problem to check compatibility. + + Returns: + Returns True if the problem is compatible, False otherwise. + """ + return len(self.get_compatibility_msg(problem)) == 0 diff --git a/qiskit/optimization/exceptions.py b/qiskit/optimization/exceptions.py new file mode 100644 index 000000000..aa14a50fe --- /dev/null +++ b/qiskit/optimization/exceptions.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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 Exception """ + +from qiskit.aqua.aqua_error import AquaError + + +class QiskitOptimizationError(AquaError): + """Class for errors returned by Qiskit Optimization libraries functions.""" + pass diff --git a/qiskit/optimization/infinity.py b/qiskit/optimization/infinity.py new file mode 100644 index 000000000..a0fefc26e --- /dev/null +++ b/qiskit/optimization/infinity.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""Infinity constant.""" + + +INFINITY = 1.0E+20 # pylint: disable=invalid-name diff --git a/qiskit/optimization/problems/__init__.py b/qiskit/optimization/problems/__init__.py new file mode 100644 index 000000000..f0b0495df --- /dev/null +++ b/qiskit/optimization/problems/__init__.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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 stack for Aqua (:mod:`qiskit.optimization.problems`) +================================================================= + +.. currentmodule:: qiskit.optimization.problems + +Structures for defining an optimization problem and its solution +================================================================ + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + Constraint + LinearExpression + LinearConstraint + QuadraticExpression + QuadraticConstraint + QuadraticObjective + QuadraticProgram + Variable + +N.B. All classes but `QuadraticProgram` are not to be instantiated directly. +Objects of those types are available within an instantiated `QuadraticProgram`. + +""" + +from .constraint import Constraint +from .linear_constraint import LinearConstraint +from .linear_expression import LinearExpression +from .quadratic_constraint import QuadraticConstraint +from .quadratic_expression import QuadraticExpression +from .quadratic_objective import QuadraticObjective +from .quadratic_program import QuadraticProgram +from .variable import Variable + +__all__ = ['Constraint', + 'LinearExpression', + 'LinearConstraint', + 'QuadraticExpression', + 'QuadraticConstraint', + 'QuadraticObjective', + 'QuadraticProgram', + 'Variable', + ] diff --git a/qiskit/optimization/problems/constraint.py b/qiskit/optimization/problems/constraint.py new file mode 100644 index 000000000..fa7390beb --- /dev/null +++ b/qiskit/optimization/problems/constraint.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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 Constraint.""" + +from abc import abstractmethod +from enum import Enum +from typing import Union, List, Dict + +from numpy import ndarray + +from .quadratic_program_element import QuadraticProgramElement +from ..exceptions import QiskitOptimizationError + + +class ConstraintSense(Enum): + """Constants Sense Type.""" + + # pylint: disable=invalid-name + LE = 0 + GE = 1 + EQ = 2 + + @staticmethod + def convert(sense: Union[str, 'ConstraintSense']) -> 'ConstraintSense': + """Convert a string into a corresponding sense of constraints + + Args: + sense: A string or sense of constraints + + Returns: + The sense of constraints + + Raises: + QiskitOptimizationError: if the input string is invalid. + """ + if isinstance(sense, ConstraintSense): + return sense + sense = sense.upper() + if sense not in ['E', 'L', 'G', 'EQ', 'LE', 'GE', '=', '==', '<=', '<', '>=', '>']: + raise QiskitOptimizationError('Invalid sense: {}'.format(sense)) + if sense in ['E', 'EQ', '=', '==']: + return ConstraintSense.EQ + elif sense in ['L', 'LE', '<=', '<']: + return ConstraintSense.LE + else: + return ConstraintSense.GE + + +class Constraint(QuadraticProgramElement): + """Abstract Constraint Class.""" + + Sense = ConstraintSense + + def __init__(self, quadratic_program: 'QuadraticProgram', name: str, sense: ConstraintSense, + rhs: float) -> None: + """ Initializes the constraint. + + Args: + quadratic_program: The parent QuadraticProgram. + name: The name of the constraint. + sense: The sense of the constraint. + rhs: The right-hand-side of the constraint. + """ + super().__init__(quadratic_program) + self._name = name + self._sense = sense + self._rhs = rhs + + @property + def name(self) -> str: + """Returns the name of the constraint. + + Returns: + The name of the constraint. + """ + return self._name + + @property + def sense(self) -> ConstraintSense: + """Returns the sense of the constraint. + + Returns: + The sense of the constraint. + """ + return self._sense + + @sense.setter + def sense(self, sense: ConstraintSense) -> None: + """Sets the sense of the constraint. + + Args: + sense: The sense of the constraint. + """ + self._sense = sense + + @property + def rhs(self) -> float: + """Returns the right-hand-side of the constraint. + + Returns: + The right-hand-side of the constraint. + """ + return self._rhs + + @rhs.setter + def rhs(self, rhs: float) -> None: + """Sets the right-hand-side of the constraint. + + Args: + rhs: The right-hand-side of the constraint. + """ + self._rhs = rhs + + @abstractmethod + def evaluate(self, x: Union[ndarray, List, Dict[Union[int, str], float]]) -> float: + """Evaluate left-hand-side of constraint for given values of variables. + + Args: + x: The values to be used for the variables. + + Returns: + The left-hand-side of the constraint. + """ + raise NotImplementedError() diff --git a/qiskit/optimization/problems/linear_constraint.py b/qiskit/optimization/problems/linear_constraint.py new file mode 100644 index 000000000..467d30f95 --- /dev/null +++ b/qiskit/optimization/problems/linear_constraint.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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. + +"""Linear Constraint.""" + +from typing import Union, List, Dict + +from numpy import ndarray +from scipy.sparse import spmatrix + +from qiskit.optimization.problems.constraint import Constraint +from qiskit.optimization.problems.linear_expression import LinearExpression + + +class LinearConstraint(Constraint): + """ Representation of a linear constraint.""" + + def __init__(self, + quadratic_program: "QuadraticProgram", name: str, + linear: Union[ndarray, spmatrix, List[float], Dict[Union[str, int], float]], + sense: Constraint.Sense, + rhs: float + ) -> None: + """ + Args: + quadratic_program: The parent quadratic program. + name: The name of the constraint. + linear: The coefficients specifying the linear constraint. + sense: The sense of the constraint. + rhs: The right-hand-side of the constraint. + """ + super().__init__(quadratic_program, name, sense, rhs) + self._linear = LinearExpression(quadratic_program, linear) + + @property + def linear(self) -> LinearExpression: + """Returns the linear expression corresponding to the left-hand-side of the constraint. + + Returns: + The left-hand-side linear expression. + """ + return self._linear + + @linear.setter + def linear(self, linear: Union[ndarray, spmatrix, List[float], Dict[Union[str, int], float]]) \ + -> None: + """Sets the linear expression corresponding to the left-hand-side of the constraint. + The coefficients can either be given by an array, a (sparse) 1d matrix, a list or a + dictionary. + + Args: + linear: The linear coefficients of the left-hand-side. + """ + self._linear = LinearExpression(self.quadratic_program, linear) + + def evaluate(self, x: Union[ndarray, List, Dict[Union[int, str], float]]) -> float: + """Evaluate the left-hand-side of the constraint. + + Args: + x: The values of the variables to be evaluated. + + Returns: + The left-hand-side of the constraint given the variable values. + """ + return self.linear.evaluate(x) diff --git a/qiskit/optimization/problems/linear_expression.py b/qiskit/optimization/problems/linear_expression.py new file mode 100644 index 000000000..945aacbdb --- /dev/null +++ b/qiskit/optimization/problems/linear_expression.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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. + +"""Linear expression interface.""" + +from typing import List, Union, Dict + +from numpy import ndarray +from scipy.sparse import spmatrix, dok_matrix + +from qiskit.optimization import QiskitOptimizationError +from qiskit.optimization.problems.quadratic_program_element import QuadraticProgramElement + + +class LinearExpression(QuadraticProgramElement): + """ Representation of a linear expression by its coefficients.""" + + def __init__(self, quadratic_program: "QuadraticProgram", + coefficients: Union[ndarray, spmatrix, List[float], + Dict[Union[int, str], float]]) -> None: + """Creates a new linear expression. + + The linear expression can be defined via an array, a list, a sparse matrix, or a dictionary + that uses variable names or indices as keys and stores the values internally as a + dok_matrix. + + Args: + quadratic_program: The parent QuadraticProgram. + coefficients: The (sparse) representation of the coefficients. + + """ + super().__init__(quadratic_program) + self.coefficients = coefficients + + def __getitem__(self, i: Union[int, str]) -> float: + """Returns the i-th coefficient where i can be a variable name or index. + + Args: + i: the index or name of the variable corresponding to the coefficient. + + Returns: + The coefficient corresponding to the addressed variable. + """ + if isinstance(i, str): + i = self.quadratic_program.variables_index[i] + return self.coefficients[0, i] + + def __setitem__(self, i: Union[int, str], value: float) -> None: + if isinstance(i, str): + i = self.quadratic_program.variables_index[i] + self._coefficients[0, i] = value + + def _coeffs_to_dok_matrix(self, + coefficients: Union[ndarray, spmatrix, + List, Dict[Union[int, str], float]] + ) -> dok_matrix: + """Maps given 1d-coefficients to a dok_matrix. + + Args: + coefficients: The 1d-coefficients to be mapped. + + Returns: + The given 1d-coefficients as a dok_matrix + + Raises: + QiskitOptimizationError: if coefficients are given in unsupported format. + """ + if isinstance(coefficients, list) or \ + isinstance(coefficients, ndarray) and len(coefficients.shape) == 1: + coefficients = dok_matrix([coefficients]) + elif isinstance(coefficients, spmatrix): + coefficients = dok_matrix(coefficients) + elif isinstance(coefficients, dict): + coeffs = dok_matrix((1, self.quadratic_program.get_num_vars())) + for index, value in coefficients.items(): + if isinstance(index, str): + index = self.quadratic_program.variables_index[index] + coeffs[0, index] = value + coefficients = coeffs + else: + raise QiskitOptimizationError("Unsupported format for coefficients.") + return coefficients + + @property + def coefficients(self) -> dok_matrix: + """ Returns the coefficients of the linear expression. + + Returns: + The coefficients of the linear expression. + """ + return self._coefficients + + @coefficients.setter + def coefficients(self, + coefficients: Union[ndarray, spmatrix, + List[float], Dict[Union[str, int], float]] + ) -> None: + """Sets the coefficients of the linear expression. + + Args: + coefficients: The coefficients of the linear expression. + """ + self._coefficients = self._coeffs_to_dok_matrix(coefficients) + + def to_array(self) -> ndarray: + """Returns the coefficients of the linear expression as array. + + Returns: + An array with the coefficients corresponding to the linear expression. + """ + return self._coefficients.toarray()[0] + + def to_dict(self, use_name: bool = False) -> Dict[Union[int, str], float]: + """Returns the coefficients of the linear expression as dictionary, either using variable + names or indices as keys. + + Args: + use_name: Determines whether to use index or names to refer to variables. + + Returns: + An dictionary with the coefficients corresponding to the linear expression. + """ + if use_name: + return {self.quadratic_program.variables[k].name: v + for (_, k), v in self._coefficients.items()} + else: + return {k: v for (_, k), v in self._coefficients.items()} + + def evaluate(self, x: Union[ndarray, List, Dict[Union[int, str], float]]) -> float: + """Evaluate the linear expression for given variables. + + Args: + x: The values of the variables to be evaluated. + + Returns: + The value of the linear expression given the variable values. + """ + # cast input to dok_matrix if it is a dictionary + x = self._coeffs_to_dok_matrix(x) + + # compute the dot-product of the input and the linear coefficients + val = (x @ self.coefficients.transpose())[0, 0] + + # return the result + return val diff --git a/qiskit/optimization/problems/quadratic_constraint.py b/qiskit/optimization/problems/quadratic_constraint.py new file mode 100644 index 000000000..f87cbab6b --- /dev/null +++ b/qiskit/optimization/problems/quadratic_constraint.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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. + +"""Quadratic Constraint.""" + +from typing import Union, List, Dict, Tuple + +from numpy import ndarray +from scipy.sparse import spmatrix + +from qiskit.optimization.problems.constraint import Constraint +from qiskit.optimization.problems.linear_expression import LinearExpression +from qiskit.optimization.problems.quadratic_expression import QuadraticExpression + + +class QuadraticConstraint(Constraint): + """ Representation of a quadratic constraint.""" + + def __init__(self, + quadratic_program: "QuadraticProgram", name: str, + linear: Union[ndarray, spmatrix, List[float], Dict[Union[str, int], float]], + quadratic: Union[ndarray, spmatrix, List[List[float]], + Dict[Tuple[Union[int, str], Union[int, str]], float]], + sense: Constraint.Sense, + rhs: float + ) -> None: + """Constructs a quadratic constraint, consisting of a linear and a quadratic term. + + Args: + quadratic_program: The parent quadratic program. + name: The name of the constraint. + linear: The coefficients specifying the linear part of the constraint. + quadratic: The coefficients specifying the linear part of the constraint. + sense: The sense of the constraint. + rhs: The right-hand-side of the constraint. + """ + super().__init__(quadratic_program, name, sense, rhs) + self._linear = LinearExpression(quadratic_program, linear) + self._quadratic = QuadraticExpression(quadratic_program, quadratic) + + @property + def linear(self) -> LinearExpression: + """Returns the linear expression corresponding to the left-hand-side of the constraint. + + Returns: + The left-hand-side linear expression. + """ + return self._linear + + @linear.setter + def linear(self, linear: Union[ndarray, spmatrix, List[float], + Dict[Union[str, int], float]]) -> None: + """Sets the linear expression corresponding to the left-hand-side of the constraint. + The coefficients can either be given by an array, a (sparse) 1d matrix, a list or a + dictionary. + + Args: + linear: The linear coefficients of the left-hand-side. + """ + + self._linear = LinearExpression(self.quadratic_program, linear) + + @property + def quadratic(self) -> QuadraticExpression: + """Returns the quadratic expression corresponding to the left-hand-side of the constraint. + + Returns: + The left-hand-side quadratic expression. + """ + return self._quadratic + + @quadratic.setter + def quadratic(self, quadratic: Union[ndarray, spmatrix, List[List[float]], + Dict[Tuple[Union[int, str], Union[int, str]], float]]) \ + -> None: + """Sets the quadratic expression corresponding to the left-hand-side of the constraint. + The coefficients can either be given by an array, a (sparse) matrix, a list or a + dictionary. + + Args: + quadratic: The quadratic coefficients of the left-hand-side. + """ + self._linear = QuadraticExpression(self.quadratic_program, quadratic) + + def evaluate(self, x: Union[ndarray, List, Dict[Union[int, str], float]]) -> float: + """Evaluate the left-hand-side of the constraint. + + Args: + x: The values of the variables to be evaluated. + + Returns: + The left-hand-side of the constraint given the variable values. + """ + return self.linear.evaluate(x) + self.quadratic.evaluate(x) diff --git a/qiskit/optimization/problems/quadratic_expression.py b/qiskit/optimization/problems/quadratic_expression.py new file mode 100644 index 000000000..d219f1f6b --- /dev/null +++ b/qiskit/optimization/problems/quadratic_expression.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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. + +"""Quadratic expression interface.""" + +from typing import List, Union, Dict, Tuple + +import numpy as np +from numpy import ndarray +from scipy.sparse import spmatrix, dok_matrix, tril, triu + +from qiskit.optimization import QiskitOptimizationError +from qiskit.optimization.problems.quadratic_program_element import QuadraticProgramElement + + +class QuadraticExpression(QuadraticProgramElement): + """ Representation of a quadratic expression by its coefficients.""" + + def __init__(self, quadratic_program: "QuadraticProgram", + coefficients: Union[ndarray, spmatrix, List[List[float]], + Dict[Tuple[Union[int, str], Union[int, str]], float]]) -> None: + """Creates a new quadratic expression. + + The quadratic expression can be defined via an array, a list, a sparse matrix, or a + dictionary that uses variable names or indices as keys and stores the values internally as a + dok_matrix. We stores values in a compressed way, i.e., values at symmetric positions are + summed up in the upper triangle. For example, {(0, 1): 1, (1, 0): 2} -> {(0, 1): 3}. + + Args: + quadratic_program: The parent QuadraticProgram. + coefficients: The (sparse) representation of the coefficients. + + """ + super().__init__(quadratic_program) + self.coefficients = coefficients + + def __getitem__(self, key: Tuple[Union[int, str], Union[int, str]]) -> float: + """Returns the coefficient where i, j can be a variable names or indices. + + Args: + key: The tuple of indices or names of the variables corresponding to the coefficient. + + Returns: + The coefficient corresponding to the addressed variables. + """ + i, j = key + if isinstance(i, str): + i = self.quadratic_program.variables_index[i] + if isinstance(j, str): + j = self.quadratic_program.variables_index[j] + return self.coefficients[min(i, j), max(i, j)] + + def __setitem__(self, key: Tuple[Union[int, str], Union[int, str]], value: float) -> None: + """Sets the coefficient where i, j can be a variable names or indices. + + Args: + key: The tuple of indices or names of the variables corresponding to the coefficient. + value: The coefficient corresponding to the addressed variables. + """ + i, j = key + if isinstance(i, str): + i = self.quadratic_program.variables_index[i] + if isinstance(j, str): + j = self.quadratic_program.variables_index[j] + self.coefficients[min(i, j), max(i, j)] = value + + def _coeffs_to_dok_matrix(self, + coefficients: Union[ndarray, spmatrix, List[List[float]], + Dict[Tuple[Union[int, str], Union[int, str]], + float]]) -> dok_matrix: + """Maps given coefficients to a dok_matrix. + + Args: + coefficients: The coefficients to be mapped. + + Returns: + The given coefficients as a dok_matrix + + Raises: + QiskitOptimizationError: if coefficients are given in unsupported format. + """ + if isinstance(coefficients, (list, ndarray, spmatrix)): + coefficients = dok_matrix(coefficients) + elif isinstance(coefficients, dict): + n = self.quadratic_program.get_num_vars() + coeffs = dok_matrix((n, n)) + for (i, j), value in coefficients.items(): + if isinstance(i, str): + i = self.quadratic_program.variables_index[i] + if isinstance(j, str): + j = self.quadratic_program.variables_index[j] + coeffs[i, j] = value + coefficients = coeffs + else: + raise QiskitOptimizationError( + "Unsupported format for coefficients: {}".format(coefficients)) + return self._triangle_matrix(coefficients) + + @staticmethod + def _triangle_matrix(mat: dok_matrix) -> dok_matrix: + lower = tril(mat, -1, format='dok') + # `todok` is necessary because subtraction results in other format + return (mat + lower.transpose() - lower).todok() + + @staticmethod + def _symmetric_matrix(mat: dok_matrix) -> dok_matrix: + upper = triu(mat, 1, format='dok') / 2 + # `todok` is necessary because subtraction results in other format + return (mat + upper.transpose() - upper).todok() + + @property + def coefficients(self) -> dok_matrix: + """ Returns the coefficients of the quadratic expression. + + Returns: + The coefficients of the quadratic expression. + """ + return self._coefficients + + @coefficients.setter + def coefficients(self, + coefficients: Union[ndarray, spmatrix, List[List[float]], + Dict[Tuple[Union[int, str], Union[int, str]], float]] + ) -> None: + """Sets the coefficients of the quadratic expression. + + Args: + coefficients: The coefficients of the quadratic expression. + """ + self._coefficients = self._coeffs_to_dok_matrix(coefficients) + + def to_array(self, symmetric: bool = False) -> ndarray: + """Returns the coefficients of the quadratic expression as array. + + Args: + symmetric: Determines whether the output is in a symmetric form or not. + + Returns: + An array with the coefficients corresponding to the quadratic expression. + """ + coeffs = self._symmetric_matrix(self._coefficients) if symmetric else self._coefficients + return coeffs.toarray() + + def to_dict(self, symmetric: bool = False, use_name: bool = False) \ + -> Dict[Union[Tuple[int, int], Tuple[str, str]], float]: + """Returns the coefficients of the quadratic expression as dictionary, either using tuples + of variable names or indices as keys. + + Args: + symmetric: Determines whether the output is in a symmetric form or not. + use_name: Determines whether to use index or names to refer to variables. + + Returns: + An dictionary with the coefficients corresponding to the quadratic expression. + """ + coeffs = self._symmetric_matrix(self._coefficients) if symmetric else self._coefficients + if use_name: + return {(self.quadratic_program.variables[i].name, + self.quadratic_program.variables[j].name): v + for (i, j), v in coeffs.items()} + else: + return dict(coeffs.items()) + + def evaluate(self, x: Union[ndarray, List, Dict[Union[int, str], float]]) -> float: + """Evaluate the quadratic expression for given variables: x * Q * x. + + Args: + x: The values of the variables to be evaluated. + + Returns: + The value of the quadratic expression given the variable values. + """ + # cast input to dok_matrix if it is a dictionary + if isinstance(x, dict): + x_aux = np.zeros(self.quadratic_program.get_num_vars()) + for i, v in x.items(): + if isinstance(i, str): + i = self.quadratic_program.variables_index[i] + x_aux[i] = v + x = x_aux + if isinstance(x, List): + x = np.array(x) + + # compute x * Q * x for the quadratic expression + val = x @ self.coefficients @ x + + # return the result + return val diff --git a/qiskit/optimization/problems/quadratic_objective.py b/qiskit/optimization/problems/quadratic_objective.py new file mode 100644 index 000000000..7d2f9f32f --- /dev/null +++ b/qiskit/optimization/problems/quadratic_objective.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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. + +"""Quadratic Objective.""" + +from enum import Enum +from typing import Union, List, Dict, Tuple + +from numpy import ndarray +from scipy.sparse import spmatrix + +from qiskit.optimization.problems.quadratic_program_element import QuadraticProgramElement +from qiskit.optimization.problems.linear_constraint import LinearExpression +from qiskit.optimization.problems.quadratic_expression import QuadraticExpression + + +class ObjSense(Enum): + """Objective Sense Type.""" + MINIMIZE = 1 + MAXIMIZE = -1 + + +class QuadraticObjective(QuadraticProgramElement): + """Representation of quadratic objective function of the form: + constant + linear * x + x * quadratic * x. + """ + + Sense = ObjSense + + def __init__(self, quadratic_program: "QuadraticProgram", + constant: float = 0.0, + linear: Union[ndarray, spmatrix, List[float], Dict[Union[str, int], float]] = None, + quadratic: Union[ndarray, spmatrix, List[List[float]], + Dict[Tuple[Union[int, str], Union[int, str]], float]] = None, + sense: ObjSense = ObjSense.MINIMIZE + ) -> None: + """Constructs a quadratic objective function. + + Args: + quadratic_program: The parent quadratic program. + constant: The constant offset of the objective. + linear: The coefficients of the linear part of the objective. + quadratic: The coefficients of the quadratic part of the objective. + sense: The optimization sense of the objective. + """ + super().__init__(quadratic_program) + self._constant = constant + if linear is None: + linear = {} + self._linear = LinearExpression(quadratic_program, linear) + if quadratic is None: + quadratic = {} + self._quadratic = QuadraticExpression(quadratic_program, quadratic) + self._sense = sense + + @property + def constant(self) -> float: + """Returns the constant part of the objective function. + + Returns: + The constant part of the objective function. + """ + return self._constant + + @constant.setter + def constant(self, constant: float) -> None: + """Sets the constant part of the objective function. + + Args: + constant: The constant part of the objective function. + """ + self._constant = constant + + @property + def linear(self) -> LinearExpression: + """Returns the linear part of the objective function. + + Returns: + The linear part of the objective function. + """ + return self._linear + + @linear.setter + def linear(self, linear: Union[ndarray, spmatrix, List[float], Dict[Union[str, int], float]] + ) -> None: + """Sets the coefficients of the linear part of the objective function. + + Args: + linear: The coefficients of the linear part of the objective function. + + """ + self._linear = LinearExpression(self.quadratic_program, linear) + + @property + def quadratic(self) -> QuadraticExpression: + """Returns the quadratic part of the objective function. + + Returns: + The quadratic part of the objective function. + """ + return self._quadratic + + @quadratic.setter + def quadratic(self, quadratic: Union[ndarray, spmatrix, List[List[float]], + Dict[Tuple[Union[int, str], Union[int, str]], float]] + ) -> None: + """Sets the coefficients of the quadratic part of the objective function. + + Args: + quadratic: The coefficients of the quadratic part of the objective function. + + """ + self._quadratic = QuadraticExpression(self.quadratic_program, quadratic) + + @property + def sense(self) -> ObjSense: + """Returns the sense of the objective function. + + Returns: + The sense of the objective function. + """ + return self._sense + + @sense.setter + def sense(self, sense: ObjSense) -> None: + """Sets the sense of the objective function. + + Args: + sense: The sense of the objective function. + """ + self._sense = sense + + def evaluate(self, x: Union[ndarray, List, Dict[Union[int, str], float]]) -> float: + """Evaluate the quadratic objective for given variable values. + + Args: + x: The values of the variables to be evaluated. + + Returns: + The value of the quadratic objective given the variable values. + """ + return self.constant + self.linear.evaluate(x) + self.quadratic.evaluate(x) diff --git a/qiskit/optimization/problems/quadratic_program.py b/qiskit/optimization/problems/quadratic_program.py new file mode 100644 index 000000000..90c010912 --- /dev/null +++ b/qiskit/optimization/problems/quadratic_program.py @@ -0,0 +1,1116 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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. + +"""Quadratic Program.""" +import logging +from collections import defaultdict +from enum import Enum +from math import fsum +from typing import List, Union, Dict, Optional, Tuple + +from docplex.mp.constr import (LinearConstraint as DocplexLinearConstraint, + QuadraticConstraint as DocplexQuadraticConstraint, + NotEqualConstraint) +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 numpy import ndarray +from scipy.sparse import spmatrix + +from qiskit.optimization import INFINITY, QiskitOptimizationError +from qiskit.optimization.problems.constraint import Constraint +from qiskit.optimization.problems.linear_constraint import LinearConstraint +from qiskit.optimization.problems.linear_expression import LinearExpression +from qiskit.optimization.problems.quadratic_constraint import QuadraticConstraint +from qiskit.optimization.problems.quadratic_expression import QuadraticExpression +from qiskit.optimization.problems.quadratic_objective import QuadraticObjective +from qiskit.optimization.problems.variable import Variable + +logger = logging.getLogger(__name__) + + +class QuadraticProgramStatus(Enum): + """Status of QuadraticProgram""" + VALID = 0 + INFEASIBLE = 1 + + +class QuadraticProgram: + """Representation of a Quadratically Constrained Quadratic Program supporting inequality and + equality constraints as well as continuous, binary, and integer variables. + """ + + Status = QuadraticProgramStatus + + def __init__(self, name: str = '') -> None: + """Constructs a quadratic program. + + Args: + name: The name of the quadratic program. + """ + self._name = name + self._status = QuadraticProgram.Status.VALID + + self._variables: List[Variable] = [] + self._variables_index: Dict[str, int] = {} + + self._linear_constraints: List[LinearConstraint] = [] + self._linear_constraints_index: Dict[str, int] = {} + + self._quadratic_constraints: List[QuadraticConstraint] = [] + self._quadratic_constraints_index: Dict[str, int] = {} + + self._objective = QuadraticObjective(self) + + def clear(self) -> None: + """Clears the quadratic program, i.e., deletes all variables, constraints, the + objective function as well as the name. + """ + self._name = '' + self._status = QuadraticProgram.Status.VALID + + self._variables.clear() + self._variables_index.clear() + + self._linear_constraints.clear() + self._linear_constraints_index.clear() + + self._quadratic_constraints.clear() + self._quadratic_constraints_index.clear() + + self._objective = QuadraticObjective(self) + + @property + def name(self) -> str: + """Returns the name of the quadratic program. + + Returns: + The name of the quadratic program. + """ + return self._name + + @name.setter + def name(self, name: str) -> None: + """Sets the name of the quadratic program. + + Args: + name: The name of the quadratic program. + """ + self._name = name + + @property + def status(self) -> QuadraticProgramStatus: + """Status of the quadratic program. + It can be infeasible due to variable substitution. + + Returns: + The status of the quadratic program + """ + return self._status + + @property + def variables(self) -> List[Variable]: + """Returns the list of variables of the quadratic program. + + Returns: + List of variables. + """ + return self._variables + + @property + def variables_index(self) -> Dict[str, int]: + """Returns the dictionary that maps the name of a variable to its index. + + Returns: + The variable index dictionary. + """ + return self._variables_index + + def _add_variable(self, + lowerbound: Union[float, int] = 0, + upperbound: Union[float, int] = INFINITY, + vartype: Variable.Type = Variable.Type.CONTINUOUS, + name: Optional[str] = None) -> Variable: + """Checks whether a variable name is already taken and adds the variable to list and index + if not. + + Args: + lowerbound: The lowerbound of the variable. + upperbound: The upperbound of the variable. + vartype: The type of the variable. + name: The name of the variable. + + Returns: + The added variable. + + Raises: + QiskitOptimizationError: if the variable name is already taken. + + """ + if name: + if name in self._variables_index: + raise QiskitOptimizationError("Variable name already exists: {}".format(name)) + else: + k = self.get_num_vars() + while 'x{}'.format(k) in self._variables_index: + k += 1 + name = 'x{}'.format(k) + self.variables_index[name] = len(self.variables) + variable = Variable(self, name, lowerbound, upperbound, vartype) + self.variables.append(variable) + return variable + + def continuous_var(self, lowerbound: Union[float, int] = 0, + upperbound: Union[float, int] = INFINITY, + name: Optional[str] = None) -> Variable: + """Adds a continuous variable to the quadratic program. + + Args: + lowerbound: The lowerbound of the variable. + upperbound: The upperbound of the variable. + name: The name of the variable. + + Returns: + The added variable. + + Raises: + QiskitOptimizationError: if the variable name is already occupied. + """ + return self._add_variable(lowerbound, upperbound, Variable.Type.CONTINUOUS, name) + + def binary_var(self, name: Optional[str] = None) -> Variable: + """Adds a binary variable to the quadratic program. + + Args: + name: The name of the variable. + + Returns: + The added variable. + + Raises: + QiskitOptimizationError: if the variable name is already occupied. + """ + return self._add_variable(0, 1, Variable.Type.BINARY, name) + + def integer_var(self, lowerbound: Union[float, int] = 0, + upperbound: Union[float, int] = INFINITY, + name: Optional[str] = None) -> Variable: + """Adds an integer variable to the quadratic program. + + Args: + lowerbound: The lowerbound of the variable. + upperbound: The upperbound of the variable. + name: The name of the variable. + + Returns: + The added variable. + + Raises: + QiskitOptimizationError: if the variable name is already occupied. + """ + return self._add_variable(lowerbound, upperbound, Variable.Type.INTEGER, name) + + def get_variable(self, i: Union[int, str]) -> Variable: + """Returns a variable for a given name or index. + + Args: + i: the index or name of the variable. + + Returns: + The corresponding variable. + """ + if isinstance(i, int): + return self.variables[i] + else: + return self.variables[self._variables_index[i]] + + def get_num_vars(self, vartype: Optional[Variable.Type] = None) -> int: + """Returns the total number of variables or the number of variables of the specified type. + + Args: + vartype: The type to be filtered on. All variables are counted if None. + + Returns: + The total number of variables. + """ + if vartype: + return sum(variable.vartype == vartype for variable in self._variables) + else: + return len(self._variables) + + def get_num_continuous_vars(self) -> int: + """Returns the total number of continuous variables. + + Returns: + The total number of continuous variables. + """ + return self.get_num_vars(Variable.Type.CONTINUOUS) + + def get_num_binary_vars(self) -> int: + """Returns the total number of binary variables. + + Returns: + The total number of binary variables. + """ + return self.get_num_vars(Variable.Type.BINARY) + + def get_num_integer_vars(self) -> int: + """Returns the total number of integer variables. + + Returns: + The total number of integer variables. + """ + return self.get_num_vars(Variable.Type.INTEGER) + + @property + def linear_constraints(self) -> List[LinearConstraint]: + """Returns the list of linear constraints of the quadratic program. + + Returns: + List of linear constraints. + """ + return self._linear_constraints + + @property + def linear_constraints_index(self) -> Dict[str, int]: + """Returns the dictionary that maps the name of a linear constraint to its index. + + Returns: + The linear constraint index dictionary. + """ + return self._linear_constraints_index + + def linear_constraint(self, + linear: Union[ndarray, spmatrix, List[float], + Dict[Union[int, str], float]] = None, + sense: Union[str, Constraint.Sense] = '<=', + rhs: float = 0.0, name: Optional[str] = None) -> LinearConstraint: + """Adds a linear equality constraint to the quadratic program of the form: + linear * x sense rhs. + + Args: + linear: The linear coefficients of the left-hand-side of the constraint. + sense: The sense of the constraint, + - '==', '=', 'E', and 'EQ' denote 'equal to'. + - '>=', '>', 'G', and 'GE' denote 'greater-than-or-equal-to'. + - '<=', '<', 'L', and 'LE' denote 'less-than-or-equal-to'. + rhs: The right hand side of the constraint. + name: The name of the constraint. + + Returns: + The added constraint. + + Raises: + QiskitOptimizationError: if the constraint name already exists or the sense is not + valid. + """ + if name: + if name in self.linear_constraints_index: + raise QiskitOptimizationError( + "Linear constraint's name already exists: {}".format(name)) + else: + k = self.get_num_linear_constraints() + while 'c{}'.format(k) in self.linear_constraints_index: + k += 1 + name = 'c{}'.format(k) + self.linear_constraints_index[name] = len(self.linear_constraints) + if linear is None: + linear = {} + constraint = LinearConstraint(self, name, linear, Constraint.Sense.convert(sense), rhs) + self.linear_constraints.append(constraint) + return constraint + + def get_linear_constraint(self, i: Union[int, str]) -> LinearConstraint: + """Returns a linear constraint for a given name or index. + + Args: + i: the index or name of the constraint. + + Returns: + The corresponding constraint. + + Raises: + IndexError: if the index is out of the list size + KeyError: if the name does not exist + """ + if isinstance(i, int): + return self._linear_constraints[i] + else: + return self._linear_constraints[self._linear_constraints_index[i]] + + def get_num_linear_constraints(self) -> int: + """Returns the number of linear constraints. + + Returns: + The number of linear constraints. + """ + return len(self._linear_constraints) + + @property + def quadratic_constraints(self) -> List[QuadraticConstraint]: + """Returns the list of quadratic constraints of the quadratic program. + + Returns: + List of quadratic constraints. + """ + return self._quadratic_constraints + + @property + def quadratic_constraints_index(self) -> Dict[str, int]: + """Returns the dictionary that maps the name of a quadratic constraint to its index. + + Returns: + The quadratic constraint index dictionary. + """ + return self._quadratic_constraints_index + + def quadratic_constraint(self, + linear: Union[ndarray, spmatrix, List[float], + Dict[Union[int, str], float]] = None, + quadratic: Union[ndarray, spmatrix, List[List[float]], + Dict[Tuple[Union[int, str], + Union[int, str]], float]] = None, + sense: Union[str, Constraint.Sense] = '<=', + rhs: float = 0.0, name: Optional[str] = None) -> QuadraticConstraint: + """Adds a quadratic equality constraint to the quadratic program of the form: + x * Q * x <= rhs. + + Args: + linear: The linear coefficients of the constraint. + quadratic: The quadratic coefficients of the constraint. + sense: The sense of the constraint, + - '==', '=', 'E', and 'EQ' denote 'equal to'. + - '>=', '>', 'G', and 'GE' denote 'greater-than-or-equal-to'. + - '<=', '<', 'L', and 'LE' denote 'less-than-or-equal-to'. + rhs: The right hand side of the constraint. + name: The name of the constraint. + + Returns: + The added constraint. + + Raises: + QiskitOptimizationError: if the constraint name already exists. + """ + if name: + if name in self.quadratic_constraints_index: + raise QiskitOptimizationError( + "Quadratic constraint name already exists: {}".format(name)) + else: + k = self.get_num_quadratic_constraints() + while 'q{}'.format(k) in self.quadratic_constraints_index: + k += 1 + name = 'q{}'.format(k) + self.quadratic_constraints_index[name] = len(self.quadratic_constraints) + if linear is None: + linear = {} + if quadratic is None: + quadratic = {} + constraint = QuadraticConstraint(self, name, linear, quadratic, + Constraint.Sense.convert(sense), rhs) + self.quadratic_constraints.append(constraint) + return constraint + + def get_quadratic_constraint(self, i: Union[int, str]) -> QuadraticConstraint: + """Returns a quadratic constraint for a given name or index. + + Args: + i: the index or name of the constraint. + + Returns: + The corresponding constraint. + + Raises: + IndexError: if the index is out of the list size + KeyError: if the name does not exist + """ + if isinstance(i, int): + return self._quadratic_constraints[i] + else: + return self._quadratic_constraints[self._quadratic_constraints_index[i]] + + def get_num_quadratic_constraints(self) -> int: + """Returns the number of quadratic constraints. + + Returns: + The number of quadratic constraints. + """ + return len(self._quadratic_constraints) + + def remove_linear_constraint(self, i: Union[str, int]) -> None: + """Remove a linear constraint + + Args: + i: an index or a name of a linear constraint + + Raises: + KeyError: if name does not exist + IndexError: if index is out of range + """ + if isinstance(i, str): + i = self._linear_constraints_index[i] + del self._linear_constraints[i] + self._linear_constraints_index = {cst.name: j for j, cst in + enumerate(self._linear_constraints)} + + def remove_quadratic_constraint(self, i: Union[str, int]) -> None: + """Remove a quadratic constraint + + Args: + i: an index or a name of a quadratic constraint + + Raises: + KeyError: if name does not exist + IndexError: if index is out of range + """ + if isinstance(i, str): + i = self._quadratic_constraints_index[i] + del self._quadratic_constraints[i] + self._quadratic_constraints_index = {cst.name: j for j, cst in + enumerate(self._quadratic_constraints)} + + @property + def objective(self) -> QuadraticObjective: + """Returns the quadratic objective. + + Returns: + The quadratic objective. + """ + return self._objective + + def minimize(self, + constant: float = 0.0, + linear: Union[ndarray, spmatrix, List[float], Dict[Union[str, int], float]] = None, + quadratic: Union[ndarray, spmatrix, List[List[float]], + Dict[Tuple[Union[int, str], Union[int, str]], float]] = None + ) -> None: + """Sets a quadratic objective to be minimized. + + Args: + constant: the constant offset of the objective. + linear: the coefficients of the linear part of the objective. + quadratic: the coefficients of the quadratic part of the objective. + + Returns: + The created quadratic objective. + """ + self._objective = QuadraticObjective(self, constant, linear, quadratic, + QuadraticObjective.Sense.MINIMIZE) + + def maximize(self, + constant: float = 0.0, + linear: Union[ndarray, spmatrix, List[float], Dict[Union[str, int], float]] = None, + quadratic: Union[ndarray, spmatrix, List[List[float]], + Dict[Tuple[Union[int, str], Union[int, str]], float]] = None + ) -> None: + """Sets a quadratic objective to be maximized. + + Args: + constant: the constant offset of the objective. + linear: the coefficients of the linear part of the objective. + quadratic: the coefficients of the quadratic part of the objective. + + Returns: + The created quadratic objective. + """ + self._objective = QuadraticObjective(self, constant, linear, quadratic, + QuadraticObjective.Sense.MAXIMIZE) + + 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. + """ + + # 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 x.get_vartype().one_letter_symbol() == 'C': + x_new = self.continuous_var(x.lb, x.ub, x.name) + var_names[x] = x_new.name + elif x.get_vartype().one_letter_symbol() == 'B': + x_new = self.binary_var(x.name) + var_names[x] = x_new.name + elif x.get_vartype().one_letter_symbol() == 'I': + x_new = self.integer_var(x.lb, x.ub, x.name) + var_names[x] = x_new.name + else: + raise QiskitOptimizationError( + "Unsupported variable type: {} {}".format(x.name, x.vartype)) + + # 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.generate_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 Aqua optimization. + raise QiskitOptimizationError( + 'Unsupported constraint: {}'.format(constraint)) + name = constraint.name + sense = constraint.sense + + rhs = 0 + if not isinstance(constraint.lhs, Var): + rhs -= constraint.lhs.constant + if not isinstance(constraint.rhs, Var): + rhs += constraint.rhs.constant + + lhs = {} + for x in constraint.iter_net_linear_coefs(): + lhs[x[0].name] = x[1] + + 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)) + + # 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: + 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)) + + def to_docplex(self) -> Model: + """Returns a docplex model corresponding to this quadratic program. + + Returns: + The docplex model corresponding to this quadratic program. + + Raises: + QiskitOptimizationError: if non-supported elements (should never happen). + """ + + # initialize model + mdl = Model(self.name) + + # add variables + var = {} + for i, x in enumerate(self.variables): + if x.vartype == Variable.Type.CONTINUOUS: + var[i] = mdl.continuous_var(lb=x.lowerbound, ub=x.upperbound, name=x.name) + elif x.vartype == Variable.Type.BINARY: + var[i] = mdl.binary_var(name=x.name) + elif x.vartype == Variable.Type.INTEGER: + var[i] = 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[i] + for (i, j), v in self.objective.quadratic.to_dict().items(): + objective += v * var[i] * var[j] + if self.objective.sense == QuadraticObjective.Sense.MINIMIZE: + mdl.minimize(objective) + else: + mdl.maximize(objective) + + # add linear constraints + for i, constraint in enumerate(self.linear_constraints): + name = constraint.name + rhs = constraint.rhs + if rhs == 0 and constraint.linear.coefficients.nnz == 0: + continue + linear_expr = 0 + for j, v in constraint.linear.to_dict().items(): + linear_expr += v * var[j] + sense = 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, constraint in enumerate(self.quadratic_constraints): + name = constraint.name + rhs = constraint.rhs + if rhs == 0 \ + and constraint.linear.coefficients.nnz == 0 \ + and constraint.quadratic.coefficients.nnz == 0: + continue + quadratic_expr = 0 + for j, v in constraint.linear.to_dict().items(): + quadratic_expr += v * var[j] + for (j, k), v in constraint.quadratic.to_dict().items(): + quadratic_expr += v * var[j] * var[k] + sense = 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 pprint_as_string(self) -> str: + """Returns the quadratic program as a string in Docplex's pretty print format. + + Returns: + A string representing the quadratic program. + """ + return self.to_docplex().pprint_as_string() + + def prettyprint(self, out: Optional[str] = None) -> None: + """Pretty prints the quadratic program to a given output stream (None = default). + + Args: + out: The output stream or file name to print to. + if you specify a file name, the output file name is has '.mod' as suffix. + """ + self.to_docplex().prettyprint(out) + + def export_as_lp_string(self) -> str: + """Returns the quadratic program as a string of LP format. + + Returns: + A string representing the quadratic program. + """ + return self.to_docplex().export_as_lp_string() + + def read_from_lp_file(self, filename: str) -> None: + """Loads the quadratic program from a LP file. + + Args: + filename: The filename of the file to be loaded. + + Raises: + FileNotFoundError: if the file does not exist. + """ + + def _parse_problem_name(filename: str) -> str: + # Because docplex model reader uses the base name as model name, + # we parse the model name in the LP file manually. + # https://ibmdecisionoptimization.github.io/docplex-doc/mp/docplex.mp.model_reader.html + prefix = '\\Problem name:' + model_name = '' + with open(filename) as file: + for line in file: + if line.startswith(prefix): + model_name = line[len(prefix):].strip() + if not line.startswith('\\'): + break + return model_name + + model_reader = ModelReader() + model = model_reader.read(pathname=filename, model_name=_parse_problem_name(filename)) + self.from_docplex(model) + + def write_to_lp_file(self, filename: str) -> None: + """Writes the quadratic program to an LP file. + + Args: + filename: The filename of the file the model is written to. + If filename is a directory, file name 'my_problem.lp' is appended. + If filename does not end with '.lp', suffix '.lp' is appended. + + Raises: + OSError: If this cannot open a file. + DOcplexException: If filename is an empty string + """ + self.to_docplex().export_as_lp(filename) + + def substitute_variables( + self, constants: Optional[Dict[Union[str, int], float]] = None, + variables: Optional[Dict[Union[str, int], Tuple[Union[str, int], float]]] = None) \ + -> 'QuadraticProgram': + """Substitutes variables with constants or other variables. + + Args: + constants: replace variable by constant + e.g., {'x': 2} means 'x' is substituted with 2 + + variables: replace variables by weighted other variable + need to copy everything using name reference to make sure that indices are matched + correctly. The lower and upper bounds are updated accordingly. + e.g., {'x': ('y', 2)} means 'x' is substituted with 'y' * 2 + + Returns: + An optimization problem by substituting variables with constants or other variables. + If the substitution is valid, `QuadraticProgram.status` is still + `QuadraticProgram.Status.VALIAD`. + Otherwise, it gets `QuadraticProgram.Status.INFEASIBLE`. + + Raises: + QiskitOptimizationError: if the substitution is invalid as follows. + - Same variable is substituted multiple times. + - Coefficient of variable substitution is zero. + """ + return SubstituteVariables().substitute_variables(self, constants, variables) + + +class SubstituteVariables: + """A class to substitute variables of an optimization problem with constants for other + variables""" + + CONST = '__CONSTANT__' + + def __init__(self): + self._src: Optional[QuadraticProgram] = None + self._dst: Optional[QuadraticProgram] = None + self._subs = {} + + def substitute_variables( + self, src: QuadraticProgram, + constants: Optional[Dict[Union[str, int], float]] = None, + variables: Optional[Dict[Union[str, int], Tuple[Union[str, int], float]]] = None) \ + -> QuadraticProgram: + """Substitutes variables with constants or other variables. + + Args: + src: a quadratic program to be substituted. + + constants: replace variable by constant + e.g., {'x': 2} means 'x' is substituted with 2 + + variables: replace variables by weighted other variable + need to copy everything using name reference to make sure that indices are matched + correctly. The lower and upper bounds are updated accordingly. + e.g., {'x': ('y', 2)} means 'x' is substituted with 'y' * 2 + + Returns: + An optimization problem by substituting variables with constants or other variables. + If the substitution is valid, `QuadraticProgram.status` is still + `QuadraticProgram.Status.VALIAD`. + Otherwise, it gets `QuadraticProgram.Status.INFEASIBLE`. + + Raises: + QiskitOptimizationError: if the substitution is invalid as follows. + - Same variable is substituted multiple times. + - Coefficient of variable substitution is zero. + """ + self._src = src + self._dst = QuadraticProgram(src.name) + self._subs_dict(constants, variables) + results = [ + self._variables(), + self._objective(), + self._linear_constraints(), + self._quadratic_constraints(), + ] + if any(not r for r in results): + self._dst._status = QuadraticProgram.Status.INFEASIBLE + return self._dst + + @staticmethod + def _feasible(sense: Constraint.Sense, rhs: float) -> bool: + """Checks feasibility of the following condition + 0 `sense` rhs + """ + # I use the following pylint option because `rhs` should come to right + # pylint: disable=misplaced-comparison-constant + if sense == Constraint.Sense.EQ: + if 0 == rhs: + return True + elif sense == Constraint.Sense.LE: + if 0 <= rhs: + return True + elif sense == Constraint.Sense.GE: + if 0 >= rhs: + return True + return False + + @staticmethod + def _replace_dict_keys_with_names(op, dic): + key = [] + val = [] + for k in sorted(dic.keys()): + key.append(op.variables.get_names(k)) + val.append(dic[k]) + return key, val + + def _subs_dict(self, constants, variables): + # guarantee that there is no overlap between variables to be replaced and combine input + subs: Dict[str, Tuple[str, float]] = {} + if constants is not None: + for i, v in constants.items(): + # substitute i <- v + i_2 = self._src.get_variable(i).name + if i_2 in subs: + raise QiskitOptimizationError( + 'Cannot substitute the same variable twice: {} <- {}'.format(i, v)) + subs[i_2] = (self.CONST, v) + + if variables is not None: + for i, (j, v) in variables.items(): + if v == 0: + raise QiskitOptimizationError( + 'coefficient must be non-zero: {} {} {}'.format(i, j, v)) + # substitute i <- j * v + i_2 = self._src.get_variable(i).name + j_2 = self._src.get_variable(j).name + if i_2 == j_2: + raise QiskitOptimizationError( + 'Cannot substitute the same variable: {} <- {} {}'.format(i, j, v)) + if i_2 in subs: + raise QiskitOptimizationError( + 'Cannot substitute the same variable twice: {} <- {} {}'.format(i, j, v)) + if j_2 in subs: + raise QiskitOptimizationError( + 'Cannot substitute by variable that gets substituted itself: ' + '{} <- {} {}'.format(i, j, v)) + subs[i_2] = (j_2, v) + + self._subs = subs + + def _variables(self) -> bool: + # copy variables that are not replaced + feasible = True + for var in self._src.variables: + name = var.name + vartype = var.vartype + lowerbound = var.lowerbound + upperbound = var.upperbound + if name not in self._subs: + self._dst._add_variable(lowerbound, upperbound, vartype, name) + + for i, (j, v) in self._subs.items(): + lb_i = self._src.get_variable(i).lowerbound + ub_i = self._src.get_variable(i).upperbound + if j == self.CONST: + if not lb_i <= v <= ub_i: + logger.warning( + 'Infeasible substitution for variable: %s', i) + feasible = False + else: + # substitute i <- j * v + # lb_i <= i <= ub_i --> lb_i / v <= j <= ub_i / v if v > 0 + # ub_i / v <= j <= lb_i / v if v < 0 + if v == 0: + raise QiskitOptimizationError( + 'Coefficient of variable substitution should be nonzero: ' + '{} {} {}'.format(i, j, v)) + if abs(lb_i) < INFINITY: + new_lb_i = lb_i / v + else: + new_lb_i = lb_i if v > 0 else -lb_i + if abs(ub_i) < INFINITY: + new_ub_i = ub_i / v + else: + new_ub_i = ub_i if v > 0 else -ub_i + var_j = self._dst.get_variable(j) + lb_j = var_j.lowerbound + ub_j = var_j.upperbound + if v > 0: + var_j.lowerbound = max(lb_j, new_lb_i) + var_j.upperbound = min(ub_j, new_ub_i) + else: + var_j.lowerbound = max(lb_j, new_ub_i) + var_j.upperbound = min(ub_j, new_lb_i) + + for var in self._dst.variables: + if var.lowerbound > var.upperbound: + logger.warning( + 'Infeasible lower and upper bound: %s %f %f', var, var.lowerbound, + var.upperbound) + feasible = False + + return feasible + + def _linear_expression(self, lin_expr: LinearExpression) \ + -> Tuple[List[float], LinearExpression]: + const = [] + lin_dict = defaultdict(float) + for i, w_i in lin_expr.to_dict(use_name=True).items(): + repl_i = self._subs[i] if i in self._subs else (i, 1) + prod = w_i * repl_i[1] + if repl_i[0] == self.CONST: + const.append(prod) + else: + k = repl_i[0] + lin_dict[k] += prod + new_lin = LinearExpression(quadratic_program=self._dst, + coefficients=lin_dict if lin_dict else {}) + return const, new_lin + + def _quadratic_expression(self, quad_expr: QuadraticExpression) \ + -> Tuple[List[float], Optional[LinearExpression], Optional[QuadraticExpression]]: + const = [] + lin_dict = defaultdict(float) + quad_dict = defaultdict(float) + for (i, j), w_ij in quad_expr.to_dict(use_name=True).items(): + repl_i = self._subs[i] if i in self._subs else (i, 1) + repl_j = self._subs[j] if j in self._subs else (j, 1) + idx = tuple(x for x, _ in [repl_i, repl_j] if x != self.CONST) + prod = w_ij * repl_i[1] * repl_j[1] + if len(idx) == 2: + quad_dict[idx] += prod + elif len(idx) == 1: + lin_dict[idx[0]] += prod + else: + const.append(prod) + new_lin = LinearExpression(quadratic_program=self._dst, + coefficients=lin_dict if lin_dict else {}) + new_quad = QuadraticExpression(quadratic_program=self._dst, + coefficients=quad_dict if quad_dict else {}) + return const, new_lin, new_quad + + def _objective(self) -> bool: + obj = self._src.objective + const1, lin1 = self._linear_expression(obj.linear) + const2, lin2, quadratic = self._quadratic_expression(obj.quadratic) + + constant = fsum([obj.constant] + const1 + const2) + linear = lin1.coefficients + lin2.coefficients + if obj.sense == obj.sense.MINIMIZE: + self._dst.minimize(constant=constant, linear=linear, quadratic=quadratic.coefficients) + else: + self._dst.maximize(constant=constant, linear=linear, quadratic=quadratic.coefficients) + return True + + def _linear_constraints(self) -> bool: + feasible = True + for lin_cst in self._src.linear_constraints: + constant, linear = self._linear_expression(lin_cst.linear) + rhs = -fsum([-lin_cst.rhs] + constant) + if linear.coefficients.nnz > 0: + self._dst.linear_constraint(name=lin_cst.name, linear=linear.coefficients, + sense=lin_cst.sense, rhs=rhs) + else: + if not self._feasible(lin_cst.sense, rhs): + logger.warning('constraint %s is infeasible due to substitution', lin_cst.name) + feasible = False + return feasible + + def _quadratic_constraints(self) -> bool: + feasible = True + for quad_cst in self._src.quadratic_constraints: + const1, lin1 = self._linear_expression(quad_cst.linear) + const2, lin2, quadratic = self._quadratic_expression(quad_cst.quadratic) + rhs = -fsum([-quad_cst.rhs] + const1 + const2) + linear = lin1.coefficients + lin2.coefficients + + if quadratic.coefficients.nnz > 0: + self._dst.quadratic_constraint(name=quad_cst.name, linear=linear, + quadratic=quadratic.coefficients, + sense=quad_cst.sense, rhs=rhs) + elif linear.nnz > 0: + name = quad_cst.name + lin_names = set(lin.name for lin in self._dst.linear_constraints) + while name in lin_names: + name = '_' + name + self._dst.linear_constraint(name=name, linear=linear, sense=quad_cst.sense, rhs=rhs) + else: + if not self._feasible(quad_cst.sense, rhs): + logger.warning('constraint %s is infeasible due to substitution', quad_cst.name) + feasible = False + + return feasible diff --git a/qiskit/optimization/problems/quadratic_program_element.py b/qiskit/optimization/problems/quadratic_program_element.py new file mode 100644 index 000000000..f17bea036 --- /dev/null +++ b/qiskit/optimization/problems/quadratic_program_element.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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. + +"""Interface for all objects that have a parent QuadraticProgram.""" + + +class QuadraticProgramElement: + """Interface class for all objects that have a parent QuadraticProgram.""" + + def __init__(self, quadratic_program: "QuadraticProgram") -> None: + """ Initialize object with parent QuadraticProgram. + + Args: + quadratic_program: The parent QuadraticProgram. + """ + self._quadratic_program = quadratic_program + + @property + def quadratic_program(self) -> "QuadraticProgram": + """Returns the parent QuadraticProgram. + + Returns: + The parent QuadraticProgram. + """ + return self._quadratic_program + + @quadratic_program.setter + def quadratic_program(self, quadratic_program: "QuadraticProgram") -> None: + """Sets the parent QuadraticProgram. + + Args: + quadratic_program: The parent QuadraticProgram. + """ + self._quadratic_program = quadratic_program diff --git a/qiskit/optimization/problems/variable.py b/qiskit/optimization/problems/variable.py new file mode 100644 index 000000000..97cd8e685 --- /dev/null +++ b/qiskit/optimization/problems/variable.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2020. +# +# 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. + +"""Variable interface""" + +from enum import Enum +from typing import Tuple, Union + +from qiskit.optimization import INFINITY, QiskitOptimizationError +from qiskit.optimization.problems.quadratic_program_element import QuadraticProgramElement + + +class VarType(Enum): + """Constants defining variable type.""" + CONTINUOUS = 0 + BINARY = 1 + INTEGER = 2 + + +class Variable(QuadraticProgramElement): + """Representation of a variable.""" + + Type = VarType + + def __init__(self, quadratic_program: 'QuadraticProgram', name: str, + lowerbound: Union[float, int] = 0, + upperbound: Union[float, int] = INFINITY, + vartype: VarType = VarType.CONTINUOUS) -> None: + """Creates a new Variable. + + The variables is exposed by the top-level `QuadraticProgram` class + in `QuadraticProgram.variables`. This constructor is not meant to be used + externally. + + Args: + quadratic_program: The parent QuadraticProgram. + name: The variable name. + lowerbound: The variable lowerbound. + upperbound: The variable upperbound. + vartype: The variable type. + + Raises: + QiskitOptimizationError: if lowerbound is greater than upperbound. + """ + if lowerbound > upperbound: + raise QiskitOptimizationError("Lowerbound is greater than upperbound!") + + super().__init__(quadratic_program) + self._name = name + self._lowerbound = lowerbound + self._upperbound = upperbound + self._vartype = vartype + + @property + def name(self) -> str: + """Returns the name of the variable. + + Returns: + The name of the variable. + """ + return self._name + + @property + def lowerbound(self) -> Union[float, int]: + """Returns the lowerbound of the variable. + + Returns: + The lower bound of the variable. + """ + return self._lowerbound + + @lowerbound.setter + def lowerbound(self, lowerbound: Union[float, int]) -> None: + """Sets the lowerbound of the variable. + + Args: + lowerbound: The lower bound of the variable. + + Raises: + QiskitOptimizationError: if lowerbound is greater than upperbound. + """ + if lowerbound > self.upperbound: + raise QiskitOptimizationError("Lowerbound is greater than upperbound!") + self._lowerbound = lowerbound + + @property + def upperbound(self) -> Union[float, int]: + """Returns the upperbound of the variable. + + Returns: + The upperbound of the variable. + """ + return self._upperbound + + @upperbound.setter + def upperbound(self, upperbound: Union[float, int]) -> None: + """Sets the upperbound of the variable. + + Args: + upperbound: The upperbound of the variable. + + Raises: + QiskitOptimizationError: if upperbound is smaller than lowerbound. + """ + if self.lowerbound > upperbound: + raise QiskitOptimizationError("Lowerbound is greater than upperbound!") + self._upperbound = upperbound + + @property + def vartype(self) -> VarType: + """Returns the type of the variable. + + Returns: + The variable type. + + """ + return self._vartype + + @vartype.setter + def vartype(self, vartype: VarType) -> None: + """Sets the type of the variable. + + Args: + vartype: The variable type. + """ + self._vartype = vartype + + def as_tuple(self) -> Tuple[str, Union[float, int], Union[float, int], VarType]: + """ Returns a tuple corresponding to this variable. + + Returns: + A tuple corresponding to this variable consisting of name, lowerbound, upperbound and + variable type. + """ + return self.name, self.lowerbound, self.upperbound, self.vartype diff --git a/test/optimization/__init__.py b/test/optimization/__init__.py old mode 100644 new mode 100755 diff --git a/test/optimization/optimization_test_case.py b/test/optimization/optimization_test_case.py old mode 100644 new mode 100755 diff --git a/test/optimization/resources/op_ip1.lp b/test/optimization/resources/op_ip1.lp new file mode 100644 index 000000000..13a8bc3b9 --- /dev/null +++ b/test/optimization/resources/op_ip1.lp @@ -0,0 +1,13 @@ +\ENCODING=ISO-8859-1 +\Problem name: op_ip1 + +Minimize + obj1: 2 x0 + 2 x1 + [ 4 x0 * x0 + x1 * x1 + 0.5 x0 * x1 + 0.5 x0 * x1 ] / 2 +Subject To + constraint: x0 + x1 = 2 +Bounds + 0 <= x0 <= 4 + 0 <= x1 <= 4 +Generals + x0 x1 +End diff --git a/test/optimization/resources/op_ip2.lp b/test/optimization/resources/op_ip2.lp new file mode 100644 index 000000000..3a9923848 --- /dev/null +++ b/test/optimization/resources/op_ip2.lp @@ -0,0 +1,12 @@ +\ENCODING=ISO-8859-1 +\Problem name: op_ip2 + +Maximize + obj: x1 + 2 x2 + 3 x3 + [ - 33 x1 ^2 + 12 x1 * x2 - 22 x2 ^2 + 23 x2 * x3 + - 11 x3 ^2 ] / 2 +Subject To + c1: - x1 + x2 + x3 <= 20 + c2: x1 - 3 x2 + x3 <= 30 +Bounds + 0 <= x1 <= 40 +End \ No newline at end of file diff --git a/test/optimization/resources/op_lp1.lp b/test/optimization/resources/op_lp1.lp new file mode 100644 index 000000000..56979102a --- /dev/null +++ b/test/optimization/resources/op_lp1.lp @@ -0,0 +1,11 @@ +\ENCODING=ISO-8859-1 +\Problem name: op_ip1 + +Minimize + obj: 2 x0 + 2 x1 + [ 4 x0 * x0 + x1 * x1 + 0.5 x0 * x1 + 0.5 x0 * x1 ] / 2 +Subject To + constraint: x0 + x1 = 2 +Bounds + 0 <= x0 <= 4 + 0 <= x1 <= 4 +End diff --git a/test/optimization/resources/op_mip1.lp b/test/optimization/resources/op_mip1.lp new file mode 100644 index 000000000..9d26bd32d --- /dev/null +++ b/test/optimization/resources/op_mip1.lp @@ -0,0 +1,13 @@ +\ENCODING=ISO-8859-1 +\Problem name: op_ip1 + +Minimize + obj1: x0 + 2 x1 + 4 x2 + [ 4 x0 * x0 + x1 * x1 + 0.5 x0 * x1 + 0.5 x0 * x1 ] / 2 +Subject To + constraint: x0 + x1 + x2 = 2 +Bounds + 0 <= x0 <= 4 + 0 <= x1 <= 4 +Generals + x0 x1 +End diff --git a/test/optimization/resources/test_quadratic_program.lp b/test/optimization/resources/test_quadratic_program.lp new file mode 100644 index 000000000..6ffd73214 --- /dev/null +++ b/test/optimization/resources/test_quadratic_program.lp @@ -0,0 +1,25 @@ +\ This file has been generated by DOcplex +\ ENCODING=ISO-8859-1 +\Problem name: my problem + +Minimize + obj: x - y + 10 z + [ x^2 - 2 y*z ]/2 + 1 +Subject To + lin_eq: x + 2 y = 1 + lin_leq: x + 2 y <= 1 + lin_geq: x + 2 y >= 1 + quad_eq: [ x^2 - y*z + 2 z^2 ] + x + y = 1 + quad_leq: [ x^2 - y*z + 2 z^2 ] + x + y <= 1 + quad_geq: [ x^2 - y*z + 2 z^2 ] + x + y >= 1 + +Bounds + 0 <= x <= 1 + -1 <= y <= 5 + -1 <= z <= 5 + +Binaries + x + +Generals + y +End diff --git a/test/optimization/test_admm.py b/test/optimization/test_admm.py new file mode 100644 index 000000000..6c6543e1c --- /dev/null +++ b/test/optimization/test_admm.py @@ -0,0 +1,327 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""Tests of the ADMM algorithm.""" +from test.optimization import QiskitOptimizationTestCase + +import numpy as np +from docplex.mp.model import Model +from qiskit.aqua.algorithms import NumPyMinimumEigensolver +from qiskit.optimization.algorithms import CplexOptimizer, MinimumEigenOptimizer +from qiskit.optimization.algorithms.admm_optimizer import ADMMOptimizer, ADMMParameters, \ + ADMMOptimizationResult, ADMMState +from qiskit.optimization.problems import QuadraticProgram + + +class TestADMMOptimizer(QiskitOptimizationTestCase): + """ADMM Optimizer Tests""" + + def test_admm_maximization(self): + """Tests a simple maximization problem using ADMM optimizer""" + try: + mdl = Model('simple-max') + c = mdl.continuous_var(lb=0, ub=10, name='c') + x = mdl.binary_var(name='x') + mdl.maximize(c + x * x) + op = QuadraticProgram() + op.from_docplex(mdl) + + admm_params = ADMMParameters() + + qubo_optimizer = MinimumEigenOptimizer(NumPyMinimumEigensolver()) + # qubo_optimizer = CplexOptimizer() + continuous_optimizer = CplexOptimizer() + + solver = ADMMOptimizer(qubo_optimizer=qubo_optimizer, + continuous_optimizer=continuous_optimizer, + params=admm_params) + solution: ADMMOptimizationResult = solver.solve(op) + self.assertIsNotNone(solution) + self.assertIsInstance(solution, ADMMOptimizationResult) + + self.assertIsNotNone(solution.x) + np.testing.assert_almost_equal([10, 0], solution.x, 3) + self.assertIsNotNone(solution.fval) + np.testing.assert_almost_equal(10, solution.fval, 3) + self.assertIsNotNone(solution.state) + self.assertIsInstance(solution.state, ADMMState) + except NameError as ex: + self.skipTest(str(ex)) + + def test_admm_ex4(self): + """Example 4 as a unit test. Example 4 is reported in: + Gambella, C., & Simonetto, A. (2020). + Multi-block ADMM Heuristics for Mixed-Binary Optimization on Classical + and Quantum Computers. + arXiv preprint arXiv:2001.02069.""" + try: + mdl = Model('ex4') + + v = mdl.binary_var(name='v') + w = mdl.binary_var(name='w') + # pylint:disable=invalid-name + t = mdl.binary_var(name='t') + + # b = 1 + b = 2 + + mdl.minimize(v + w + t) + mdl.add_constraint(2 * v + 10 * w + t <= 3, "cons1") + mdl.add_constraint(v + w + t >= b, "cons2") + + op = QuadraticProgram() + op.from_docplex(mdl) + + # qubo_optimizer = MinimumEigenOptimizer(NumPyMinimumEigensolver()) + qubo_optimizer = CplexOptimizer() + continuous_optimizer = CplexOptimizer() + + admm_params = ADMMParameters( + rho_initial=1001, beta=1000, factor_c=900, + max_iter=100, three_block=False + ) + + solver = ADMMOptimizer(params=admm_params, qubo_optimizer=qubo_optimizer, + continuous_optimizer=continuous_optimizer, ) + solution = solver.solve(op) + self.assertIsNotNone(solution) + self.assertIsInstance(solution, ADMMOptimizationResult) + self.assertIsNotNone(solution.x) + np.testing.assert_almost_equal([1., 0., 1.], solution.x, 3) + self.assertIsNotNone(solution.fval) + np.testing.assert_almost_equal(2., solution.fval, 3) + self.assertIsNotNone(solution.state) + self.assertIsInstance(solution.state, ADMMState) + + except NameError as ex: + self.skipTest(str(ex)) + + def test_admm_ex5(self): + """Example 5 as a unit test. Example 5 is reported in: + Gambella, C., & Simonetto, A. (2020). + Multi-block ADMM Heuristics for Mixed-Binary Optimization on Classical + and Quantum Computers. + arXiv preprint arXiv:2001.02069.""" + try: + mdl = Model('ex5') + + # pylint:disable=invalid-name + v = mdl.binary_var(name='v') + w = mdl.binary_var(name='w') + t = mdl.binary_var(name='t') + + mdl.minimize(v + w + t) + mdl.add_constraint(2 * v + 2 * w + t <= 3, "cons1") + mdl.add_constraint(v + w + t >= 1, "cons2") + mdl.add_constraint(v + w == 1, "cons3") + + op = QuadraticProgram() + op.from_docplex(mdl) + + qubo_optimizer = CplexOptimizer() + + continuous_optimizer = CplexOptimizer() + + admm_params = ADMMParameters( + rho_initial=1001, beta=1000, factor_c=900, + max_iter=100, three_block=False + ) + + solver = ADMMOptimizer(params=admm_params, qubo_optimizer=qubo_optimizer, + continuous_optimizer=continuous_optimizer, ) + solution = solver.solve(op) + + self.assertIsNotNone(solution) + self.assertIsInstance(solution, ADMMOptimizationResult) + self.assertIsNotNone(solution.x) + np.testing.assert_almost_equal([1., 0., 1.], solution.x, 3) + self.assertIsNotNone(solution.fval) + np.testing.assert_almost_equal(2., solution.fval, 3) + self.assertIsNotNone(solution.state) + self.assertIsInstance(solution.state, ADMMState) + except NameError as ex: + self.skipTest(str(ex)) + + def test_admm_ex6(self): + """Example 6 as a unit test. Example 6 is reported in: + Gambella, C., & Simonetto, A. (2020). + Multi-block ADMM Heuristics for Mixed-Binary Optimization on Classical + and Quantum Computers. + arXiv preprint arXiv:2001.02069.""" + try: + mdl = Model('ex6') + + # pylint:disable=invalid-name + v = mdl.binary_var(name='v') + w = mdl.binary_var(name='w') + t = mdl.binary_var(name='t') + u = mdl.continuous_var(name='u') + + mdl.minimize(v + w + t + 5 * (u - 2) ** 2) + mdl.add_constraint(v + 2 * w + t + u <= 3, "cons1") + mdl.add_constraint(v + w + t >= 1, "cons2") + mdl.add_constraint(v + w == 1, "cons3") + + op = QuadraticProgram() + op.from_docplex(mdl) + + qubo_optimizer = CplexOptimizer() + continuous_optimizer = CplexOptimizer() + + admm_params = ADMMParameters( + rho_initial=1001, beta=1000, factor_c=900, + max_iter=100, three_block=True, tol=1.e-6 + ) + + solver = ADMMOptimizer(params=admm_params, qubo_optimizer=qubo_optimizer, + continuous_optimizer=continuous_optimizer) + solution = solver.solve(op) + + self.assertIsNotNone(solution) + self.assertIsInstance(solution, ADMMOptimizationResult) + self.assertIsNotNone(solution.x) + np.testing.assert_almost_equal([1., 0., 0., 2.], solution.x, 3) + self.assertIsNotNone(solution.fval) + np.testing.assert_almost_equal(1., solution.fval, 3) + self.assertIsNotNone(solution.state) + self.assertIsInstance(solution.state, ADMMState) + except NameError as ex: + self.skipTest(str(ex)) + + def test_admm_ex6_max(self): + """Example 6 as maximization""" + try: + mdl = Model('ex6-max') + + # pylint:disable=invalid-name + v = mdl.binary_var(name='v') + w = mdl.binary_var(name='w') + t = mdl.binary_var(name='t') + u = mdl.continuous_var(name='u') + + # mdl.minimize(v + w + t + 5 * (u - 2) ** 2) + mdl.maximize(- v - w - t - 5 * (u - 2) ** 2) + mdl.add_constraint(v + 2 * w + t + u <= 3, "cons1") + mdl.add_constraint(v + w + t >= 1, "cons2") + mdl.add_constraint(v + w == 1, "cons3") + + op = QuadraticProgram() + op.from_docplex(mdl) + + qubo_optimizer = CplexOptimizer() + continuous_optimizer = CplexOptimizer() + + admm_params = ADMMParameters( + rho_initial=1001, beta=1000, factor_c=900, + max_iter=100, three_block=True, tol=1.e-6 + ) + + solver = ADMMOptimizer(params=admm_params, qubo_optimizer=qubo_optimizer, + continuous_optimizer=continuous_optimizer) + solution = solver.solve(op) + + self.assertIsNotNone(solution) + self.assertIsInstance(solution, ADMMOptimizationResult) + self.assertIsNotNone(solution.x) + np.testing.assert_almost_equal([1., 0., 0., 2.], solution.x, 3) + self.assertIsNotNone(solution.fval) + np.testing.assert_almost_equal(-1., solution.fval, 3) + self.assertIsNotNone(solution.state) + self.assertIsInstance(solution.state, ADMMState) + except NameError as ex: + self.skipTest(str(ex)) + + def test_equality_constraints_with_continuous_variables(self): + """Simple example to test equality constraints with continuous variables.""" + try: + mdl = Model("eq-constraints-cts-vars") + + # pylint:disable=invalid-name + v = mdl.binary_var(name='v') + w = mdl.continuous_var(name='w', lb=0.) + t = mdl.continuous_var(name='t', lb=0.) + + mdl.minimize(v + w + t) + mdl.add_constraint(2 * v + w >= 2, "cons1") + mdl.add_constraint(w + t == 1, "cons2") + + op = QuadraticProgram() + op.from_docplex(mdl) + + # qubo_optimizer = MinimumEigenOptimizer(NumPyMinimumEigensolver()) + qubo_optimizer = CplexOptimizer() + + continuous_optimizer = CplexOptimizer() + + admm_params = ADMMParameters( + rho_initial=1001, beta=1000, factor_c=900, + max_iter=100, three_block=True, + ) + + solver = ADMMOptimizer(params=admm_params, qubo_optimizer=qubo_optimizer, + continuous_optimizer=continuous_optimizer, ) + solution = solver.solve(op) + + self.assertIsNotNone(solution) + self.assertIsInstance(solution, ADMMOptimizationResult) + self.assertIsNotNone(solution.x) + np.testing.assert_almost_equal([0., 1., 0.], solution.x, 3) + self.assertIsNotNone(solution.fval) + np.testing.assert_almost_equal(1., solution.fval, 3) + self.assertIsNotNone(solution.state) + self.assertIsInstance(solution.state, ADMMState) + except NameError as ex: + self.skipTest(str(ex)) + + def test_quad_constraints(self): + """Simple example to test quadratic constraints.""" + + try: + mdl = Model('quad-constraints') + + v = mdl.binary_var(name='v') + w = mdl.continuous_var(name='w', lb=0.) + + mdl.minimize(v + w) + mdl.add_constraint(v + w >= 1, "cons2") + mdl.add_constraint(v ** 2 + w ** 2 <= 1, "cons2") + + op = QuadraticProgram() + op.from_docplex(mdl) + + # qubo_optimizer = MinimumEigenOptimizer(NumPyMinimumEigensolver()) + qubo_optimizer = CplexOptimizer() + + continuous_optimizer = CplexOptimizer() + + admm_params = ADMMParameters( + rho_initial=1001, beta=1000, factor_c=900, + max_iter=100, three_block=True, + ) + + solver = ADMMOptimizer(params=admm_params, qubo_optimizer=qubo_optimizer, + continuous_optimizer=continuous_optimizer, ) + solution = solver.solve(op) + + self.assertIsNotNone(solution) + self.assertIsInstance(solution, ADMMOptimizationResult) + self.assertIsNotNone(solution.x) + np.testing.assert_almost_equal([0., 1.], solution.x, 3) + self.assertIsNotNone(solution.fval) + np.testing.assert_almost_equal(1., solution.fval, 3) + self.assertIsNotNone(solution.state) + self.assertIsInstance(solution.state, ADMMState) + + except NameError as ex: + self.skipTest(str(ex)) diff --git a/test/optimization/test_classical_cplex.py b/test/optimization/test_classical_cplex.py old mode 100644 new mode 100755 index 3474f12c2..da75a59d2 --- a/test/optimization/test_classical_cplex.py +++ b/test/optimization/test_classical_cplex.py @@ -14,12 +14,13 @@ """ Test Classical Cplex """ +import unittest from test.optimization import QiskitOptimizationTestCase import numpy as np from qiskit.aqua import aqua_globals -from qiskit.optimization.ising import max_cut -from qiskit.optimization.ising.common import random_graph +from qiskit.optimization.applications.ising import max_cut +from qiskit.optimization.applications.ising.common import random_graph from qiskit.aqua.algorithms import ClassicalCPLEX @@ -45,3 +46,7 @@ def test_cplex_ising(self): self.assertEqual(max_cut.max_cut_value(x, self.w), 24) except NameError as ex: self.skipTest(str(ex)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_clique.py b/test/optimization/test_clique.py old mode 100644 new mode 100755 index 675387bd7..db99cabd3 --- a/test/optimization/test_clique.py +++ b/test/optimization/test_clique.py @@ -14,13 +14,14 @@ """ Test Clique """ +import unittest from test.optimization import QiskitOptimizationTestCase import numpy as np from qiskit import BasicAer from qiskit.aqua import aqua_globals, QuantumInstance -from qiskit.optimization.ising import clique -from qiskit.optimization.ising.common import random_graph, sample_most_likely +from qiskit.optimization.applications.ising import clique +from qiskit.optimization.applications.ising.common import random_graph, sample_most_likely from qiskit.aqua.algorithms import NumPyMinimumEigensolver, VQE from qiskit.aqua.components.optimizers import COBYLA from qiskit.aqua.components.variational_forms import RY @@ -80,3 +81,7 @@ def test_clique_vqe(self): np.testing.assert_array_equal(ising_sol, [1, 1, 1, 1, 1]) oracle = self._brute_force() self.assertEqual(clique.satisfy_or_not(ising_sol, self.w, self.k), oracle) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_cobyla_optimizer.py b/test/optimization/test_cobyla_optimizer.py new file mode 100644 index 000000000..6de49193d --- /dev/null +++ b/test/optimization/test_cobyla_optimizer.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2020. +# +# 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 Cobyla Optimizer """ + +import unittest +from test.optimization.optimization_test_case import QiskitOptimizationTestCase +import logging + +from qiskit.optimization.algorithms import CobylaOptimizer +from qiskit.optimization.problems import QuadraticProgram + +logger = logging.getLogger(__name__) + + +class TestCobylaOptimizer(QiskitOptimizationTestCase): + """Cobyla Optimizer Tests.""" + + def test_cobyla_optimizer(self): + """ Cobyla Optimizer Test. """ + + # load optimization problem + problem = QuadraticProgram() + problem.continuous_var(upperbound=4) + problem.continuous_var(upperbound=4) + problem.linear_constraint(linear=[1, 1], sense='=', rhs=2) + problem.minimize(linear=[2, 2], quadratic=[[2, 0.25], [0.25, 0.5]]) + + # solve problem with cobyla + cobyla = CobylaOptimizer() + result = cobyla.solve(problem) + + # analyze results + self.assertAlmostEqual(result.fval, 5.8750) + + def test_cobyla_optimizer_with_quadratic_constraint(self): + """ Cobyla Optimizer Test """ + # load optimization problem + problem = QuadraticProgram() + problem.continuous_var(upperbound=1) + problem.continuous_var(upperbound=1) + + problem.minimize(linear=[1, 1]) + + linear = [-1, -1] + quadratic = [[1, 0], [0, 1]] + problem.quadratic_constraint(linear=linear, quadratic=quadratic, rhs=-1/2) + + # solve problem with cobyla + cobyla = CobylaOptimizer() + result = cobyla.solve(problem) + + # analyze results + self.assertAlmostEqual(result.fval, 1.0, places=2) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_converters.py b/test/optimization/test_converters.py new file mode 100644 index 000000000..45bdf469f --- /dev/null +++ b/test/optimization/test_converters.py @@ -0,0 +1,497 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 Converters """ + +import unittest +from test.optimization.optimization_test_case import QiskitOptimizationTestCase +import logging +import numpy as np +from docplex.mp.model import Model + +from qiskit.aqua.operators import WeightedPauliOperator +from qiskit.aqua.algorithms import NumPyMinimumEigensolver +from qiskit.optimization import QuadraticProgram, QiskitOptimizationError +from qiskit.optimization.problems import Constraint, Variable +from qiskit.optimization.algorithms import OptimizationResult +from qiskit.optimization.converters import ( + InequalityToEquality, + QuadraticProgramToOperator, + OperatorToQuadraticProgram, + IntegerToBinary, + LinearEqualityToPenalty, +) +from qiskit.optimization.algorithms import MinimumEigenOptimizer, CplexOptimizer, ADMMOptimizer +from qiskit.optimization.algorithms.admm_optimizer import ADMMParameters +from qiskit.quantum_info import Pauli + +logger = logging.getLogger(__name__) + + +QUBIT_OP_MAXIMIZE_SAMPLE = WeightedPauliOperator( + paulis=[ + [(-199999.5 + 0j), Pauli(z=[True, False, False, False], x=[False, False, False, False])], + [(-399999.5 + 0j), Pauli(z=[False, True, False, False], x=[False, False, False, False])], + [(-599999.5 + 0j), Pauli(z=[False, False, True, False], x=[False, False, False, False])], + [(-799999.5 + 0j), Pauli(z=[False, False, False, True], x=[False, False, False, False])], + [(100000 + 0j), Pauli(z=[True, True, False, False], x=[False, False, False, False])], + [(150000 + 0j), Pauli(z=[True, False, True, False], x=[False, False, False, False])], + [(300000 + 0j), Pauli(z=[False, True, True, False], x=[False, False, False, False])], + [(200000 + 0j), Pauli(z=[True, False, False, True], x=[False, False, False, False])], + [(400000 + 0j), Pauli(z=[False, True, False, True], x=[False, False, False, False])], + [(600000 + 0j), Pauli(z=[False, False, True, True], x=[False, False, False, False])], + ] +) +OFFSET_MAXIMIZE_SAMPLE = 1149998 + + +class TestConverters(QiskitOptimizationTestCase): + """Test Converters""" + + def test_empty_problem(self): + """ Test empty problem """ + op = QuadraticProgram() + conv = InequalityToEquality() + op = conv.encode(op) + conv = IntegerToBinary() + op = conv.encode(op) + conv = LinearEqualityToPenalty() + op = conv.encode(op) + conv = QuadraticProgramToOperator() + _, shift = conv.encode(op) + self.assertEqual(shift, 0.0) + + def test_valid_variable_type(self): + """Validate the types of the variables for QuadraticProgramToOperator.""" + # Integer variable + with self.assertRaises(QiskitOptimizationError): + op = QuadraticProgram() + op.integer_var(0, 10, "int_var") + conv = QuadraticProgramToOperator() + _ = conv.encode(op) + # Continuous variable + with self.assertRaises(QiskitOptimizationError): + op = QuadraticProgram() + op.continuous_var(0, 10, "continuous_var") + conv = QuadraticProgramToOperator() + _ = conv.encode(op) + + def test_inequality_binary(self): + """ Test InequalityToEqualityConverter with binary variables """ + op = QuadraticProgram() + for i in range(3): + op.binary_var(name='x{}'.format(i)) + # Linear constraints + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x1'] = 1 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 1, 'x0x1') + linear_constraint = {} + linear_constraint['x1'] = 1 + linear_constraint['x2'] = -1 + op.linear_constraint(linear_constraint, Constraint.Sense.LE, 2, 'x1x2') + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x2'] = 3 + op.linear_constraint(linear_constraint, Constraint.Sense.GE, 2, 'x0x2') + # Quadratic constraints + quadratic = {} + quadratic[('x0', 'x1')] = 1 + quadratic[('x1', 'x2')] = 2 + op.quadratic_constraint({}, quadratic, Constraint.Sense.LE, 3, 'x0x1_x1x2LE') + quadratic = {} + quadratic[('x0', 'x1')] = 3 + quadratic[('x1', 'x2')] = 4 + op.quadratic_constraint({}, quadratic, Constraint.Sense.GE, 3, 'x0x1_x1x2GE') + # Convert inequality constraints into equality constraints + conv = InequalityToEquality() + op2 = conv.encode(op) + # Check names and objective senses + self.assertEqual(op.name, op2.name) + self.assertEqual(op.objective.sense, op2.objective.sense) + # For linear constraints + lst = [ + op2.linear_constraints[0].linear.to_dict()[0], + op2.linear_constraints[0].linear.to_dict()[1], + ] + self.assertListEqual(lst, [1, 1]) + self.assertEqual(op2.linear_constraints[0].sense, Constraint.Sense.EQ) + lst = [ + op2.linear_constraints[1].linear.to_dict()[1], + op2.linear_constraints[1].linear.to_dict()[2], + op2.linear_constraints[1].linear.to_dict()[3], + ] + self.assertListEqual(lst, [1, -1, 1]) + lst = [op2.variables[3].lowerbound, op2.variables[3].upperbound] + self.assertListEqual(lst, [0, 3]) + self.assertEqual(op2.linear_constraints[1].sense, Constraint.Sense.EQ) + lst = [ + op2.linear_constraints[2].linear.to_dict()[0], + op2.linear_constraints[2].linear.to_dict()[2], + op2.linear_constraints[2].linear.to_dict()[4], + ] + self.assertListEqual(lst, [1, 3, -1]) + lst = [op2.variables[4].lowerbound, op2.variables[4].upperbound] + self.assertListEqual(lst, [0, 2]) + self.assertEqual(op2.linear_constraints[2].sense, Constraint.Sense.EQ) + # For quadratic constraints + lst = [ + op2.quadratic_constraints[0].quadratic.to_dict()[(0, 1)], + op2.quadratic_constraints[0].quadratic.to_dict()[(1, 2)], + op2.quadratic_constraints[0].linear.to_dict()[5], + ] + self.assertListEqual(lst, [1, 2, 1]) + lst = [op2.variables[5].lowerbound, op2.variables[5].upperbound] + self.assertListEqual(lst, [0, 3]) + lst = [ + op2.quadratic_constraints[1].quadratic.to_dict()[(0, 1)], + op2.quadratic_constraints[1].quadratic.to_dict()[(1, 2)], + op2.quadratic_constraints[1].linear.to_dict()[6], + ] + self.assertListEqual(lst, [3, 4, -1]) + lst = [op2.variables[6].lowerbound, op2.variables[6].upperbound] + self.assertListEqual(lst, [0, 4]) + + def test_inequality_integer(self): + """ Test InequalityToEqualityConverter with integer variables """ + op = QuadraticProgram() + for i in range(3): + op.integer_var(name='x{}'.format(i), lowerbound=-3, upperbound=3) + # Linear constraints + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x1'] = 1 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 1, 'x0x1') + linear_constraint = {} + linear_constraint['x1'] = 1 + linear_constraint['x2'] = -1 + op.linear_constraint(linear_constraint, Constraint.Sense.LE, 2, 'x1x2') + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x2'] = 3 + op.linear_constraint(linear_constraint, Constraint.Sense.GE, 2, 'x0x2') + # Quadratic constraints + quadratic = {} + quadratic[('x0', 'x1')] = 1 + quadratic[('x1', 'x2')] = 2 + op.quadratic_constraint({}, quadratic, Constraint.Sense.LE, 3, 'x0x1_x1x2LE') + quadratic = {} + quadratic[('x0', 'x1')] = 3 + quadratic[('x1', 'x2')] = 4 + op.quadratic_constraint({}, quadratic, Constraint.Sense.GE, 3, 'x0x1_x1x2GE') + conv = InequalityToEquality() + op2 = conv.encode(op) + # For linear constraints + lst = [ + op2.linear_constraints[0].linear.to_dict()[0], + op2.linear_constraints[0].linear.to_dict()[1], + ] + self.assertListEqual(lst, [1, 1]) + self.assertEqual(op2.linear_constraints[0].sense, Constraint.Sense.EQ) + lst = [ + op2.linear_constraints[1].linear.to_dict()[1], + op2.linear_constraints[1].linear.to_dict()[2], + op2.linear_constraints[1].linear.to_dict()[3], + ] + self.assertListEqual(lst, [1, -1, 1]) + lst = [op2.variables[3].lowerbound, op2.variables[3].upperbound] + self.assertListEqual(lst, [0, 8]) + self.assertEqual(op2.linear_constraints[1].sense, Constraint.Sense.EQ) + lst = [ + op2.linear_constraints[2].linear.to_dict()[0], + op2.linear_constraints[2].linear.to_dict()[2], + op2.linear_constraints[2].linear.to_dict()[4], + ] + self.assertListEqual(lst, [1, 3, -1]) + lst = [op2.variables[4].lowerbound, op2.variables[4].upperbound] + self.assertListEqual(lst, [0, 10]) + self.assertEqual(op2.linear_constraints[2].sense, Constraint.Sense.EQ) + # For quadratic constraints + lst = [ + op2.quadratic_constraints[0].quadratic.to_dict()[(0, 1)], + op2.quadratic_constraints[0].quadratic.to_dict()[(1, 2)], + op2.quadratic_constraints[0].linear.to_dict()[5], + ] + self.assertListEqual(lst, [1, 2, 1]) + lst = [op2.variables[5].lowerbound, op2.variables[5].upperbound] + self.assertListEqual(lst, [0, 30]) + lst = [ + op2.quadratic_constraints[1].quadratic.to_dict()[(0, 1)], + op2.quadratic_constraints[1].quadratic.to_dict()[(1, 2)], + op2.quadratic_constraints[1].linear.to_dict()[6], + ] + self.assertListEqual(lst, [3, 4, -1]) + lst = [op2.variables[6].lowerbound, op2.variables[6].upperbound] + self.assertListEqual(lst, [0, 60]) + + def test_inequality_mode_integer(self): + """ Test integer mode of InequalityToEqualityConverter() """ + op = QuadraticProgram() + for i in range(3): + op.binary_var(name='x{}'.format(i)) + # Linear constraints + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x1'] = 1 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 1, 'x0x1') + linear_constraint = {} + linear_constraint['x1'] = 1 + linear_constraint['x2'] = -1 + op.linear_constraint(linear_constraint, Constraint.Sense.LE, 2, 'x1x2') + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x2'] = 3 + op.linear_constraint(linear_constraint, Constraint.Sense.GE, 2, 'x0x2') + conv = InequalityToEquality() + op2 = conv.encode(op, mode='integer') + lst = [op2.variables[3].vartype, op2.variables[4].vartype] + self.assertListEqual(lst, [Variable.Type.INTEGER, Variable.Type.INTEGER]) + + def test_inequality_mode_continuous(self): + """ Test continuous mode of InequalityToEqualityConverter() """ + op = QuadraticProgram() + for i in range(3): + op.binary_var(name='x{}'.format(i)) + # Linear constraints + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x1'] = 1 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 1, 'x0x1') + linear_constraint = {} + linear_constraint['x1'] = 1 + linear_constraint['x2'] = -1 + op.linear_constraint(linear_constraint, Constraint.Sense.LE, 2, 'x1x2') + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x2'] = 3 + op.linear_constraint(linear_constraint, Constraint.Sense.GE, 2, 'x0x2') + conv = InequalityToEquality() + op2 = conv.encode(op, mode='continuous') + lst = [op2.variables[3].vartype, op2.variables[4].vartype] + self.assertListEqual(lst, [Variable.Type.CONTINUOUS, Variable.Type.CONTINUOUS]) + + def test_inequality_mode_auto(self): + """ Test auto mode of InequalityToEqualityConverter() """ + op = QuadraticProgram() + for i in range(3): + op.binary_var(name='x{}'.format(i)) + # Linear constraints + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x1'] = 1 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 1, 'x0x1') + linear_constraint = {} + linear_constraint['x1'] = 1 + linear_constraint['x2'] = -1 + op.linear_constraint(linear_constraint, Constraint.Sense.LE, 2, 'x1x2') + linear_constraint = {} + linear_constraint['x0'] = 1.1 + linear_constraint['x2'] = 2.2 + op.linear_constraint(linear_constraint, Constraint.Sense.GE, 3.3, 'x0x2') + conv = InequalityToEquality() + op2 = conv.encode(op, mode='auto') + lst = [op2.variables[3].vartype, op2.variables[4].vartype] + self.assertListEqual(lst, [Variable.Type.INTEGER, Variable.Type.CONTINUOUS]) + + def test_penalize_sense(self): + """ Test PenalizeLinearEqualityConstraints with senses """ + op = QuadraticProgram() + for i in range(3): + op.binary_var(name='x{}'.format(i)) + # Linear constraints + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x1'] = 1 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 1, 'x0x1') + linear_constraint = {} + linear_constraint['x1'] = 1 + linear_constraint['x2'] = -1 + op.linear_constraint(linear_constraint, Constraint.Sense.LE, 2, 'x1x2') + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x2'] = 3 + op.linear_constraint(linear_constraint, Constraint.Sense.GE, 2, 'x0x2') + self.assertEqual(len(op.linear_constraints), 3) + conv = LinearEqualityToPenalty() + with self.assertRaises(QiskitOptimizationError): + conv.encode(op) + + def test_penalize_binary(self): + """ Test PenalizeLinearEqualityConstraints with binary variables """ + op = QuadraticProgram() + for i in range(3): + op.binary_var(name='x{}'.format(i)) + # Linear constraints + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x1'] = 1 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 1, 'x0x1') + linear_constraint = {} + linear_constraint['x1'] = 1 + linear_constraint['x2'] = -1 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 2, 'x1x2') + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x2'] = 3 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 2, 'x0x2') + self.assertEqual(len(op.linear_constraints), 3) + conv = LinearEqualityToPenalty() + op2 = conv.encode(op) + self.assertEqual(len(op2.linear_constraints), 0) + + def test_penalize_integer(self): + """ Test PenalizeLinearEqualityConstraints with integer variables """ + op = QuadraticProgram() + for i in range(3): + op.integer_var(name='x{}'.format(i), lowerbound=-3, upperbound=3) + # Linear constraints + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x1'] = 1 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 1, 'x0x1') + linear_constraint = {} + linear_constraint['x1'] = 1 + linear_constraint['x2'] = -1 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 2, 'x1x2') + linear_constraint = {} + linear_constraint['x0'] = 1 + linear_constraint['x2'] = 3 + op.linear_constraint(linear_constraint, Constraint.Sense.EQ, 2, 'x0x2') + self.assertEqual(len(op.linear_constraints), 3) + conv = LinearEqualityToPenalty() + op2 = conv.encode(op) + self.assertEqual(len(op2.linear_constraints), 0) + + def test_integer_to_binary(self): + """ Test integer to binary """ + op = QuadraticProgram() + for i in range(0, 2): + op.binary_var(name='x{}'.format(i)) + op.integer_var(name='x2', lowerbound=0, upperbound=5) + linear = {} + for i, x in enumerate(op.variables): + linear[x.name] = i + 1 + op.maximize(0, linear, {}) + conv = IntegerToBinary() + op2 = conv.encode(op) + for x in op2.variables: + self.assertEqual(x.vartype, Variable.Type.BINARY) + dct = op2.objective.linear.to_dict() + self.assertEqual(dct[2], 3) + self.assertEqual(dct[3], 6) + self.assertEqual(dct[4], 6) + + def test_binary_to_integer(self): + """ Test binary to integer """ + op = QuadraticProgram() + for i in range(0, 2): + op.binary_var(name='x{}'.format(i)) + op.integer_var(name='x2', lowerbound=0, upperbound=5) + linear = {} + linear['x0'] = 1 + linear['x1'] = 2 + linear['x2'] = 1 + op.maximize(0, linear, {}) + linear = {} + for x in op.variables: + linear[x.name] = 1 + op.linear_constraint(linear, Constraint.Sense.EQ, 6, 'x0x1x2') + conv = IntegerToBinary() + _ = conv.encode(op) + result = OptimizationResult(x=[0, 1, 1, 1, 1], fval=17) + new_result = conv.decode(result) + self.assertListEqual(new_result.x, [0, 1, 5]) + self.assertEqual(new_result.fval, 17) + + def test_optimizationproblem_to_operator(self): + """ Test optimization problem to operators""" + op = QuadraticProgram() + for i in range(4): + op.binary_var(name='x{}'.format(i)) + linear = {} + for x in op.variables: + linear[x.name] = 1 + op.maximize(0, linear, {}) + linear = {} + for i, x in enumerate(op.variables): + linear[x.name] = i + 1 + op.linear_constraint(linear, Constraint.Sense.EQ, 3, 'sum1') + penalize = LinearEqualityToPenalty() + op2ope = QuadraticProgramToOperator() + op2 = penalize.encode(op) + qubitop, offset = op2ope.encode(op2) + self.assertListEqual(qubitop.paulis, QUBIT_OP_MAXIMIZE_SAMPLE.paulis) + self.assertEqual(offset, OFFSET_MAXIMIZE_SAMPLE) + + def test_operator_to_quadraticprogram(self): + """ Test optimization problem to operators""" + op = QUBIT_OP_MAXIMIZE_SAMPLE + offset = OFFSET_MAXIMIZE_SAMPLE + + op2qp = OperatorToQuadraticProgram() + quadratic = op2qp.encode(op, offset) + + self.assertEqual(len(quadratic.variables), 4) + self.assertEqual(len(quadratic.linear_constraints), 0) + self.assertEqual(len(quadratic.quadratic_constraints), 0) + self.assertEqual(quadratic.objective.sense, quadratic.objective.Sense.MINIMIZE) + self.assertAlmostEqual(quadratic.objective.constant, 900000) + + linear_matrix = np.zeros((1, 4)) + linear_matrix[0, 0] = -500001 + linear_matrix[0, 1] = -800001 + linear_matrix[0, 2] = -900001 + linear_matrix[0, 3] = -800001 + + quadratic_matrix = np.zeros((4, 4)) + quadratic_matrix[0, 1] = 400000 + quadratic_matrix[0, 2] = 600000 + quadratic_matrix[1, 2] = 1200000 + quadratic_matrix[0, 3] = 800000 + quadratic_matrix[1, 3] = 1600000 + quadratic_matrix[2, 3] = 2400000 + + np.testing.assert_array_almost_equal(quadratic.objective.linear.coefficients.toarray(), + linear_matrix) + np.testing.assert_array_almost_equal(quadratic.objective.quadratic.coefficients.toarray(), + quadratic_matrix) + + def test_continuous_variable_decode(self): + """ Test decode func of IntegerToBinaryConverter for continuous variables""" + try: + mdl = Model('test_continuous_varable_decode') + c = mdl.continuous_var(lb=0, ub=10.9, name='c') + x = mdl.binary_var(name='x') + mdl.maximize(c + x * x) + op = QuadraticProgram() + op.from_docplex(mdl) + converter = IntegerToBinary() + op = converter.encode(op) + admm_params = ADMMParameters() + qubo_optimizer = MinimumEigenOptimizer(NumPyMinimumEigensolver()) + continuous_optimizer = CplexOptimizer() + solver = ADMMOptimizer( + qubo_optimizer=qubo_optimizer, + continuous_optimizer=continuous_optimizer, + params=admm_params, + ) + solution = solver.solve(op) + solution = converter.decode(solution) + self.assertEqual(solution.x[0], 10.9) + except NameError as ex: + self.skipTest(str(ex)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_cplex_optimizer.py b/test/optimization/test_cplex_optimizer.py new file mode 100644 index 000000000..aa11ac74d --- /dev/null +++ b/test/optimization/test_cplex_optimizer.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2020. +# +# 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 Cplex Optimizer """ + +import unittest +from test.optimization.optimization_test_case import QiskitOptimizationTestCase +from ddt import ddt, data +from qiskit.optimization.algorithms import CplexOptimizer +from qiskit.optimization.problems import QuadraticProgram + + +@ddt +class TestCplexOptimizer(QiskitOptimizationTestCase): + """Cplex Optimizer Tests.""" + + def setUp(self): + super().setUp() + try: + self.resource_path = './test/optimization/resources/' + self.cplex_optimizer = CplexOptimizer(disp=False) + except NameError 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_cplex_optimizer(self, config): + """ Cplex Optimizer Test """ + # unpack configuration + filename, x, fval = config + + # load optimization problem + problem = QuadraticProgram() + problem.read_from_lp_file(self.resource_path + filename) + + # solve problem with cplex + result = self.cplex_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/optimization/test_docplex.py b/test/optimization/test_docplex.py old mode 100644 new mode 100755 index a07f0b64d..ba295d427 --- a/test/optimization/test_docplex.py +++ b/test/optimization/test_docplex.py @@ -14,6 +14,7 @@ """ Test Docplex """ +import unittest from math import fsum, isclose from test.optimization import QiskitOptimizationTestCase @@ -24,7 +25,7 @@ from qiskit.aqua import AquaError, aqua_globals from qiskit.aqua.algorithms import NumPyMinimumEigensolver -from qiskit.optimization.ising import docplex, tsp +from qiskit.optimization.applications.ising import docplex, tsp from qiskit.aqua.operators import WeightedPauliOperator # Reference operators and offsets for maxcut and tsp. @@ -338,3 +339,7 @@ def test_constants_in_left_side_and_variables_in_right_side(self): self.assertEqual(result['eigenvalue'] + offset, -2) actual_sol = result['eigenstate'].tolist() self.assertListEqual(actual_sol, [0, 0, 0, 1]) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_exact_cover.py b/test/optimization/test_exact_cover.py old mode 100644 new mode 100755 index 9318e516b..dddfd858b --- a/test/optimization/test_exact_cover.py +++ b/test/optimization/test_exact_cover.py @@ -14,14 +14,15 @@ """ Test Exact Cover """ +import unittest import json from test.optimization import QiskitOptimizationTestCase import numpy as np from qiskit import BasicAer from qiskit.aqua import aqua_globals, QuantumInstance -from qiskit.optimization.ising import exact_cover -from qiskit.optimization.ising.common import sample_most_likely +from qiskit.optimization.applications.ising import exact_cover +from qiskit.optimization.applications.ising.common import sample_most_likely from qiskit.aqua.algorithms import NumPyMinimumEigensolver, VQE from qiskit.aqua.components.optimizers import COBYLA from qiskit.aqua.components.variational_forms import RYRZ @@ -81,3 +82,7 @@ def test_exact_cover_vqe(self): oracle = self._brute_force() self.assertEqual(exact_cover.check_solution_satisfiability(ising_sol, self.list_of_subsets), oracle) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_graph_partition.py b/test/optimization/test_graph_partition.py old mode 100644 new mode 100755 index 605400205..16a6eb1b5 --- a/test/optimization/test_graph_partition.py +++ b/test/optimization/test_graph_partition.py @@ -14,12 +14,13 @@ """ Test Graph Partition """ +import unittest from test.optimization import QiskitOptimizationTestCase import numpy as np from qiskit import BasicAer from qiskit.aqua import aqua_globals, QuantumInstance -from qiskit.optimization.ising import graph_partition -from qiskit.optimization.ising.common import random_graph, sample_most_likely +from qiskit.optimization.applications.ising import graph_partition +from qiskit.optimization.applications.ising.common import random_graph, sample_most_likely from qiskit.aqua.algorithms import NumPyMinimumEigensolver, VQE from qiskit.aqua.components.variational_forms import RY from qiskit.aqua.components.optimizers import SPSA @@ -84,3 +85,7 @@ def test_graph_partition_vqe(self): np.testing.assert_array_equal(ising_sol, [0, 1, 0, 1]) oracle = self._brute_force() self.assertEqual(graph_partition.objective_value(x, self.w), oracle) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_grover_optimizer.py b/test/optimization/test_grover_optimizer.py new file mode 100644 index 000000000..2fc8bb4bf --- /dev/null +++ b/test/optimization/test_grover_optimizer.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 Grover Optimizer.""" + +import unittest +from test.optimization import QiskitOptimizationTestCase +import random +import numpy +from docplex.mp.model import Model +from qiskit import Aer +from qiskit.aqua import aqua_globals, QuantumInstance +from qiskit.aqua.algorithms import NumPyMinimumEigensolver +from qiskit.optimization.algorithms import GroverOptimizer, MinimumEigenOptimizer +from qiskit.optimization.problems import QuadraticProgram + + +class TestGroverOptimizer(QiskitOptimizationTestCase): + """GroverOptimizer tests.""" + + def setUp(self): + super().setUp() + random.seed = 2 + numpy.random.seed = 42 + aqua_globals.seed = 42 + self.q_instance = QuantumInstance(Aer.get_backend('statevector_simulator'), + seed_simulator=921, seed_transpiler=200) + + def validate_results(self, problem, results): + """Validate the results object returned by GroverOptimizer.""" + # Get expected value. + solver = MinimumEigenOptimizer(NumPyMinimumEigensolver()) + comp_result = solver.solve(problem) + + # Validate results. + self.assertTrue(comp_result.x == results.x) + self.assertTrue(comp_result.fval == results.fval) + + def test_qubo_gas_int_zero(self): + """Test for when the answer is zero.""" + + # Input. + model = Model() + x_0 = model.binary_var(name='x0') + x_1 = model.binary_var(name='x1') + model.minimize(0*x_0+0*x_1) + op = QuadraticProgram() + op.from_docplex(model) + + # Will not find a negative, should return 0. + gmf = GroverOptimizer(1, num_iterations=1, quantum_instance=self.q_instance) + results = gmf.solve(op) + self.assertEqual(results.x, [0, 0]) + self.assertEqual(results.fval, 0.0) + + def test_qubo_gas_int_simple(self): + """Test for simple case, with 2 linear coeffs and no quadratic coeffs or constants.""" + + # Input. + model = Model() + x_0 = model.binary_var(name='x0') + x_1 = model.binary_var(name='x1') + model.minimize(-x_0+2*x_1) + op = QuadraticProgram() + op.from_docplex(model) + + # Get the optimum key and value. + n_iter = 8 + gmf = GroverOptimizer(4, num_iterations=n_iter, quantum_instance=self.q_instance) + results = gmf.solve(op) + self.validate_results(op, results) + + def test_qubo_gas_int_paper_example(self): + """Test the example from https://arxiv.org/abs/1912.04088.""" + + # Input. + model = Model() + x_0 = model.binary_var(name='x0') + x_1 = model.binary_var(name='x1') + x_2 = model.binary_var(name='x2') + model.minimize(-x_0+2*x_1-3*x_2-2*x_0*x_2-1*x_1*x_2) + op = QuadraticProgram() + op.from_docplex(model) + + # Get the optimum key and value. + n_iter = 10 + gmf = GroverOptimizer(6, num_iterations=n_iter, quantum_instance=self.q_instance) + results = gmf.solve(op) + self.validate_results(op, results) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_knapsack.py b/test/optimization/test_knapsack.py index ad0379e7f..036c668dc 100644 --- a/test/optimization/test_knapsack.py +++ b/test/optimization/test_knapsack.py @@ -14,11 +14,12 @@ """ Test Knapsack Problem """ +import unittest from test.optimization import QiskitOptimizationTestCase import numpy as np -from qiskit.optimization.ising import knapsack -from qiskit.optimization.ising.common import sample_most_likely +from qiskit.optimization.applications.ising import knapsack +from qiskit.optimization.applications.ising.common import sample_most_likely from qiskit.aqua.algorithms import NumPyMinimumEigensolver @@ -73,3 +74,7 @@ def test_knapsack_large_max_weight(self): np.testing.assert_array_equal(solution, [1, 1, 1, 1]) np.testing.assert_equal(value, 175) np.testing.assert_equal(weight, 30) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_linear_constraint.py b/test/optimization/test_linear_constraint.py new file mode 100644 index 000000000..2afdda0ce --- /dev/null +++ b/test/optimization/test_linear_constraint.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 LinearConstraint """ + +import unittest +from test.optimization.optimization_test_case import QiskitOptimizationTestCase + +import numpy as np + +from qiskit.optimization import QuadraticProgram, QiskitOptimizationError +from qiskit.optimization.problems import Constraint + + +class TestLinearConstraint(QiskitOptimizationTestCase): + """Test LinearConstraint.""" + + def test_init(self): + """ test init. """ + + quadratic_program = QuadraticProgram() + for _ in range(5): + quadratic_program.continuous_var() + self.assertEqual(quadratic_program.get_num_linear_constraints(), 0) + + coefficients = np.array(range(5)) + + # equality constraints + quadratic_program.linear_constraint(sense='==') + self.assertEqual(quadratic_program.get_num_linear_constraints(), 1) + self.assertEqual(quadratic_program.linear_constraints[0].name, 'c0') + self.assertEqual(len(quadratic_program.linear_constraints[0].linear.to_dict()), 0) + self.assertEqual(quadratic_program.linear_constraints[0].sense, Constraint.Sense.EQ) + self.assertEqual(quadratic_program.linear_constraints[0].rhs, 0.0) + self.assertEqual(quadratic_program.linear_constraints[0], + quadratic_program.get_linear_constraint('c0')) + self.assertEqual(quadratic_program.linear_constraints[0], + quadratic_program.get_linear_constraint(0)) + + with self.assertRaises(QiskitOptimizationError): + quadratic_program.linear_constraint(name='c0') + + quadratic_program.linear_constraint(coefficients, '==', 1.0, 'c1') + self.assertEqual(quadratic_program.get_num_linear_constraints(), 2) + self.assertEqual(quadratic_program.linear_constraints[1].name, 'c1') + self.assertTrue( + (quadratic_program.linear_constraints[1].linear.to_array() == coefficients).all()) + self.assertEqual(quadratic_program.linear_constraints[1].sense, Constraint.Sense.EQ) + self.assertEqual(quadratic_program.linear_constraints[1].rhs, 1.0) + self.assertEqual(quadratic_program.linear_constraints[1], + quadratic_program.get_linear_constraint('c1')) + self.assertEqual(quadratic_program.linear_constraints[1], + quadratic_program.get_linear_constraint(1)) + + # geq constraints + quadratic_program.linear_constraint(sense='>=') + self.assertEqual(quadratic_program.get_num_linear_constraints(), 3) + self.assertEqual(quadratic_program.linear_constraints[2].name, 'c2') + self.assertEqual(len(quadratic_program.linear_constraints[2].linear.to_dict()), + 0) + self.assertEqual(quadratic_program.linear_constraints[2].sense, Constraint.Sense.GE) + self.assertEqual(quadratic_program.linear_constraints[2].rhs, 0.0) + self.assertEqual(quadratic_program.linear_constraints[2], + quadratic_program.get_linear_constraint('c2')) + self.assertEqual(quadratic_program.linear_constraints[2], + quadratic_program.get_linear_constraint(2)) + + with self.assertRaises(QiskitOptimizationError): + quadratic_program.linear_constraint(name='c2', sense='>=') + + quadratic_program.linear_constraint(coefficients, '>=', 1.0, 'c3') + self.assertEqual(quadratic_program.get_num_linear_constraints(), 4) + self.assertEqual(quadratic_program.linear_constraints[3].name, 'c3') + self.assertTrue( + (quadratic_program.linear_constraints[3].linear.to_array() == coefficients).all()) + self.assertEqual(quadratic_program.linear_constraints[3].sense, Constraint.Sense.GE) + self.assertEqual(quadratic_program.linear_constraints[3].rhs, 1.0) + self.assertEqual(quadratic_program.linear_constraints[3], + quadratic_program.get_linear_constraint('c3')) + self.assertEqual(quadratic_program.linear_constraints[3], + quadratic_program.get_linear_constraint(3)) + + # leq constraints + quadratic_program.linear_constraint(sense='<=') + self.assertEqual(quadratic_program.get_num_linear_constraints(), 5) + self.assertEqual(quadratic_program.linear_constraints[4].name, 'c4') + self.assertEqual(len(quadratic_program.linear_constraints[4].linear.to_dict()), 0) + self.assertEqual(quadratic_program.linear_constraints[4].sense, Constraint.Sense.LE) + self.assertEqual(quadratic_program.linear_constraints[4].rhs, 0.0) + self.assertEqual(quadratic_program.linear_constraints[4], + quadratic_program.get_linear_constraint('c4')) + self.assertEqual(quadratic_program.linear_constraints[4], + quadratic_program.get_linear_constraint(4)) + + with self.assertRaises(QiskitOptimizationError): + quadratic_program.linear_constraint(name='c4', sense='<=') + + quadratic_program.linear_constraint(coefficients, '<=', 1.0, 'c5') + self.assertEqual(quadratic_program.get_num_linear_constraints(), 6) + self.assertEqual(quadratic_program.linear_constraints[5].name, 'c5') + self.assertTrue( + (quadratic_program.linear_constraints[5].linear.to_array() == coefficients).all()) + self.assertEqual(quadratic_program.linear_constraints[5].sense, Constraint.Sense.LE) + self.assertEqual(quadratic_program.linear_constraints[5].rhs, 1.0) + self.assertEqual(quadratic_program.linear_constraints[5], + quadratic_program.get_linear_constraint('c5')) + self.assertEqual(quadratic_program.linear_constraints[5], + quadratic_program.get_linear_constraint(5)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_linear_expression.py b/test/optimization/test_linear_expression.py new file mode 100644 index 000000000..5a7fc79fd --- /dev/null +++ b/test/optimization/test_linear_expression.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 LinearExpression """ + +import unittest + +from test.optimization.optimization_test_case import QiskitOptimizationTestCase +import numpy as np +from scipy.sparse import dok_matrix + +from qiskit.optimization import QuadraticProgram +from qiskit.optimization.problems import LinearExpression + + +class TestLinearExpression(QiskitOptimizationTestCase): + """Test LinearExpression.""" + + def test_init(self): + """ test init. """ + + quadratic_program = QuadraticProgram() + for _ in range(5): + quadratic_program.continuous_var() + + coefficients_list = list(range(5)) + coefficients_array = np.array(coefficients_list) + coefficients_dok = dok_matrix([coefficients_list]) + coefficients_dict_int = {i: i for i in range(1, 5)} + coefficients_dict_str = {'x{}'.format(i): i for i in range(1, 5)} + + for coeffs in [coefficients_list, + coefficients_array, + coefficients_dok, + coefficients_dict_int, + coefficients_dict_str]: + linear = LinearExpression(quadratic_program, coeffs) + self.assertEqual((linear.coefficients != coefficients_dok).nnz, 0) + self.assertTrue((linear.to_array() == coefficients_list).all()) + self.assertDictEqual(linear.to_dict(use_name=False), coefficients_dict_int) + self.assertDictEqual(linear.to_dict(use_name=True), coefficients_dict_str) + + def test_get_item(self): + """ test get_item. """ + + quadratic_program = QuadraticProgram() + for _ in range(5): + quadratic_program.continuous_var() + + coefficients = list(range(5)) + linear = LinearExpression(quadratic_program, coefficients) + for i, v in enumerate(coefficients): + self.assertEqual(linear[i], v) + + def test_setters(self): + """ test setters. """ + + quadratic_program = QuadraticProgram() + for _ in range(5): + quadratic_program.continuous_var() + + zeros = np.zeros(quadratic_program.get_num_vars()) + linear = LinearExpression(quadratic_program, zeros) + + coefficients_list = list(range(5)) + coefficients_array = np.array(coefficients_list) + coefficients_dok = dok_matrix([coefficients_list]) + coefficients_dict_int = {i: i for i in range(1, 5)} + coefficients_dict_str = {'x{}'.format(i): i for i in range(1, 5)} + + for coeffs in [coefficients_list, + coefficients_array, + coefficients_dok, + coefficients_dict_int, + coefficients_dict_str]: + linear.coefficients = coeffs + self.assertEqual((linear.coefficients != coefficients_dok).nnz, 0) + self.assertTrue((linear.to_array() == coefficients_list).all()) + self.assertDictEqual(linear.to_dict(use_name=False), coefficients_dict_int) + self.assertDictEqual(linear.to_dict(use_name=True), coefficients_dict_str) + + def test_evaluate(self): + """ test evaluate. """ + + quadratic_program = QuadraticProgram() + x = [quadratic_program.continuous_var() for _ in range(5)] + + coefficients_list = list(range(5)) + linear = LinearExpression(quadratic_program, coefficients_list) + + values_list = list(range(len(x))) + values_array = np.array(values_list) + values_dict_int = {i: i for i in range(len(x))} + values_dict_str = {'x{}'.format(i): i for i in range(len(x))} + + for values in [values_list, values_array, values_dict_int, values_dict_str]: + self.assertEqual(linear.evaluate(values), 30) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_min_eigen_optimizer.py b/test/optimization/test_min_eigen_optimizer.py new file mode 100644 index 000000000..01e0b3685 --- /dev/null +++ b/test/optimization/test_min_eigen_optimizer.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2020. +# +# 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 Min Eigen Optimizer """ + +import unittest +from test.optimization.optimization_test_case import QiskitOptimizationTestCase +from ddt import ddt, data + +from qiskit import BasicAer + +from qiskit.aqua.algorithms import NumPyMinimumEigensolver +from qiskit.aqua.algorithms import QAOA +from qiskit.aqua.components.optimizers import COBYLA + +from qiskit.optimization.algorithms import MinimumEigenOptimizer, CplexOptimizer +from qiskit.optimization.problems import QuadraticProgram + + +@ddt +class TestMinEigenOptimizer(QiskitOptimizationTestCase): + """Min Eigen Optimizer Tests.""" + + def setUp(self): + super().setUp() + + self.resource_path = './test/optimization/resources/' + + # setup minimum eigen solvers + self.min_eigen_solvers = {} + + # exact eigen solver + self.min_eigen_solvers['exact'] = NumPyMinimumEigensolver() + + # QAOA + optimizer = COBYLA() + self.min_eigen_solvers['qaoa'] = QAOA(optimizer=optimizer) + + @data( + ('exact', None, 'op_ip1.lp'), + ('qaoa', 'statevector_simulator', 'op_ip1.lp'), + ('qaoa', 'qasm_simulator', 'op_ip1.lp') + ) + def test_min_eigen_optimizer(self, config): + """ Min Eigen Optimizer Test """ + try: + # unpack configuration + min_eigen_solver_name, backend, filename = config + + # get minimum eigen solver + min_eigen_solver = self.min_eigen_solvers[min_eigen_solver_name] + if backend: + min_eigen_solver.quantum_instance = BasicAer.get_backend(backend) + + # construct minimum eigen optimizer + min_eigen_optimizer = MinimumEigenOptimizer(min_eigen_solver) + + # load optimization problem + problem = QuadraticProgram() + problem.read_from_lp_file(self.resource_path + filename) + + # solve problem with cplex + cplex = CplexOptimizer() + cplex_result = cplex.solve(problem) + + # solve problem + result = min_eigen_optimizer.solve(problem) + + # analyze results + self.assertAlmostEqual(cplex_result.fval, result.fval) + except RuntimeError as ex: + msg = str(ex) + if 'CPLEX' in msg: + self.skipTest(msg) + else: + self.fail(msg) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_partition.py b/test/optimization/test_partition.py old mode 100644 new mode 100755 index d9ddbf8b8..abc24262d --- a/test/optimization/test_partition.py +++ b/test/optimization/test_partition.py @@ -14,12 +14,13 @@ """ Test Partition """ +import unittest from test.optimization import QiskitOptimizationTestCase import numpy as np from qiskit import BasicAer from qiskit.aqua import aqua_globals, QuantumInstance -from qiskit.optimization.ising import partition -from qiskit.optimization.ising.common import read_numbers_from_file, sample_most_likely +from qiskit.optimization.applications.ising import partition +from qiskit.optimization.applications.ising.common import read_numbers_from_file, sample_most_likely from qiskit.aqua.algorithms import NumPyMinimumEigensolver, VQE from qiskit.aqua.components.optimizers import SPSA from qiskit.aqua.components.variational_forms import RY @@ -54,3 +55,7 @@ def test_partition_vqe(self): x = sample_most_likely(result['eigvecs'][0]) self.assertNotEqual(x[0], x[1]) self.assertNotEqual(x[2], x[1]) # hardcoded oracle + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_qaoa.py b/test/optimization/test_qaoa.py old mode 100644 new mode 100755 index ce7ab9ecc..1cdf36fc1 --- a/test/optimization/test_qaoa.py +++ b/test/optimization/test_qaoa.py @@ -21,8 +21,8 @@ from ddt import ddt, idata, unpack from qiskit import BasicAer -from qiskit.optimization.ising import max_cut -from qiskit.optimization.ising.common import sample_most_likely +from qiskit.optimization.applications.ising import max_cut +from qiskit.optimization.applications.ising.common import sample_most_likely from qiskit.aqua.components.optimizers import COBYLA from qiskit.aqua.algorithms import QAOA from qiskit.aqua import QuantumInstance, aqua_globals diff --git a/test/optimization/test_quadratic_constraint.py b/test/optimization/test_quadratic_constraint.py new file mode 100644 index 000000000..e98286798 --- /dev/null +++ b/test/optimization/test_quadratic_constraint.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 QuadraticConstraint """ + +import unittest +from test.optimization.optimization_test_case import QiskitOptimizationTestCase + +import numpy as np + +from qiskit.optimization import QuadraticProgram, QiskitOptimizationError +from qiskit.optimization.problems import Constraint + + +class TestQuadraticConstraint(QiskitOptimizationTestCase): + """Test QuadraticConstraint.""" + + def test_init(self): + """ test init. """ + + quadratic_program = QuadraticProgram() + for _ in range(5): + quadratic_program.continuous_var() + self.assertEqual(quadratic_program.get_num_quadratic_constraints(), 0) + + linear_coeffs = np.array(range(5)) + lst = [[0 for _ in range(5)] for _ in range(5)] + for i, v in enumerate(lst): + for j, _ in enumerate(v): + lst[min(i, j)][max(i, j)] += i * j + quadratic_coeffs = np.array(lst) + + # equality constraints + quadratic_program.quadratic_constraint(sense='==') + self.assertEqual(quadratic_program.get_num_quadratic_constraints(), 1) + self.assertEqual(quadratic_program.quadratic_constraints[0].name, 'q0') + self.assertEqual(len(quadratic_program.quadratic_constraints[0].linear.to_dict()), 0) + self.assertEqual(len(quadratic_program.quadratic_constraints[0].quadratic.to_dict()), 0) + self.assertEqual(quadratic_program.quadratic_constraints[0].sense, Constraint.Sense.EQ) + self.assertEqual(quadratic_program.quadratic_constraints[0].rhs, 0.0) + self.assertEqual(quadratic_program.quadratic_constraints[0], + quadratic_program.get_quadratic_constraint('q0')) + self.assertEqual(quadratic_program.quadratic_constraints[0], + quadratic_program.get_quadratic_constraint(0)) + + self.assertEqual(quadratic_program.quadratic_constraints[0].evaluate(linear_coeffs), 0.0) + + with self.assertRaises(QiskitOptimizationError): + quadratic_program.quadratic_constraint(name='q0', sense='==') + + quadratic_program.quadratic_constraint(linear_coeffs, quadratic_coeffs, '==', 1.0, 'q1') + self.assertEqual(quadratic_program.get_num_quadratic_constraints(), 2) + self.assertEqual(quadratic_program.quadratic_constraints[1].name, 'q1') + self.assertTrue( + (quadratic_program.quadratic_constraints[1].linear.to_array() == linear_coeffs).all()) + self.assertTrue( + (quadratic_program.quadratic_constraints[1].quadratic.to_array() == + quadratic_coeffs).all()) + self.assertEqual(quadratic_program.quadratic_constraints[1].sense, Constraint.Sense.EQ) + self.assertEqual(quadratic_program.quadratic_constraints[1].rhs, 1.0) + self.assertEqual(quadratic_program.quadratic_constraints[1], + quadratic_program.get_quadratic_constraint('q1')) + self.assertEqual(quadratic_program.quadratic_constraints[1], + quadratic_program.get_quadratic_constraint(1)) + + self.assertEqual(quadratic_program.quadratic_constraints[1].evaluate(linear_coeffs), 930.0) + + # geq constraints + quadratic_program.quadratic_constraint(sense='>=') + self.assertEqual(quadratic_program.get_num_quadratic_constraints(), 3) + self.assertEqual(quadratic_program.quadratic_constraints[2].name, 'q2') + self.assertEqual(len(quadratic_program.quadratic_constraints[2].linear.to_dict()), 0) + self.assertEqual(len(quadratic_program.quadratic_constraints[2].quadratic.to_dict()), 0) + self.assertEqual(quadratic_program.quadratic_constraints[2].sense, Constraint.Sense.GE) + self.assertEqual(quadratic_program.quadratic_constraints[2].rhs, 0.0) + self.assertEqual(quadratic_program.quadratic_constraints[2], + quadratic_program.get_quadratic_constraint('q2')) + self.assertEqual(quadratic_program.quadratic_constraints[2], + quadratic_program.get_quadratic_constraint(2)) + + self.assertEqual(quadratic_program.quadratic_constraints[2].evaluate(linear_coeffs), 0.0) + + with self.assertRaises(QiskitOptimizationError): + quadratic_program.quadratic_constraint(name='q2', sense='>=') + + quadratic_program.quadratic_constraint(linear_coeffs, quadratic_coeffs, '>=', 1.0, 'q3') + self.assertEqual(quadratic_program.get_num_quadratic_constraints(), 4) + self.assertEqual(quadratic_program.quadratic_constraints[3].name, 'q3') + self.assertTrue( + (quadratic_program.quadratic_constraints[3].linear.to_array() == linear_coeffs).all()) + self.assertTrue((quadratic_program.quadratic_constraints[3].quadratic.to_array() == + quadratic_coeffs).all()) + self.assertEqual(quadratic_program.quadratic_constraints[3].sense, Constraint.Sense.GE) + self.assertEqual(quadratic_program.quadratic_constraints[3].rhs, 1.0) + self.assertEqual(quadratic_program.quadratic_constraints[3], + quadratic_program.get_quadratic_constraint('q3')) + self.assertEqual(quadratic_program.quadratic_constraints[3], + quadratic_program.get_quadratic_constraint(3)) + + self.assertEqual(quadratic_program.quadratic_constraints[3].evaluate(linear_coeffs), 930.0) + + # leq constraints + quadratic_program.quadratic_constraint(sense='<=') + self.assertEqual(quadratic_program.get_num_quadratic_constraints(), 5) + self.assertEqual(quadratic_program.quadratic_constraints[4].name, 'q4') + self.assertEqual(len(quadratic_program.quadratic_constraints[4].linear.to_dict()), 0) + self.assertEqual(quadratic_program.quadratic_constraints[4].sense, Constraint.Sense.LE) + self.assertEqual(quadratic_program.quadratic_constraints[4].rhs, 0.0) + self.assertEqual(quadratic_program.quadratic_constraints[4], + quadratic_program.get_quadratic_constraint('q4')) + self.assertEqual(quadratic_program.quadratic_constraints[4], + quadratic_program.get_quadratic_constraint(4)) + + self.assertEqual(quadratic_program.quadratic_constraints[4].evaluate(linear_coeffs), 0.0) + + with self.assertRaises(QiskitOptimizationError): + quadratic_program.quadratic_constraint(name='q4', sense='<=') + + quadratic_program.quadratic_constraint(linear_coeffs, quadratic_coeffs, '<=', 1.0, 'q5') + self.assertEqual(quadratic_program.get_num_quadratic_constraints(), 6) + self.assertEqual(quadratic_program.quadratic_constraints[5].name, 'q5') + self.assertTrue( + (quadratic_program.quadratic_constraints[5].linear.to_array() == linear_coeffs).all()) + self.assertTrue((quadratic_program.quadratic_constraints[5].quadratic.to_array() == + quadratic_coeffs).all()) + self.assertEqual(quadratic_program.quadratic_constraints[5].sense, Constraint.Sense.LE) + self.assertEqual(quadratic_program.quadratic_constraints[5].rhs, 1.0) + self.assertEqual(quadratic_program.quadratic_constraints[5], + quadratic_program.get_quadratic_constraint('q5')) + self.assertEqual(quadratic_program.quadratic_constraints[5], + quadratic_program.get_quadratic_constraint(5)) + + self.assertEqual(quadratic_program.quadratic_constraints[5].evaluate(linear_coeffs), 930.0) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_quadratic_expression.py b/test/optimization/test_quadratic_expression.py new file mode 100644 index 000000000..96859d8d0 --- /dev/null +++ b/test/optimization/test_quadratic_expression.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 QuadraticExpression """ + +import unittest + +from test.optimization.optimization_test_case import QiskitOptimizationTestCase +import numpy as np +from scipy.sparse import dok_matrix + +from qiskit.optimization import QuadraticProgram +from qiskit.optimization.problems import QuadraticExpression + + +class TestQuadraticExpression(QiskitOptimizationTestCase): + """Test QuadraticExpression.""" + + def test_init(self): + """ test init. """ + + quadratic_program = QuadraticProgram() + for _ in range(5): + quadratic_program.continuous_var() + + coefficients_list = [[0 for _ in range(5)] for _ in range(5)] + for i, v in enumerate(coefficients_list): + for j, _ in enumerate(v): + coefficients_list[min(i, j)][max(i, j)] += i * j + coefficients_array = np.array(coefficients_list) + coefficients_dok = dok_matrix(coefficients_list) + coefficients_dict_int = {(i, j): v for (i, j), v in coefficients_dok.items()} + coefficients_dict_str = {('x{}'.format(i), 'x{}'.format(j)): v for (i, j), v in + coefficients_dok.items()} + + for coeffs in [coefficients_list, + coefficients_array, + coefficients_dok, + coefficients_dict_int, + coefficients_dict_str]: + quadratic = QuadraticExpression(quadratic_program, coeffs) + self.assertEqual((quadratic.coefficients != coefficients_dok).nnz, 0) + self.assertTrue((quadratic.to_array() == coefficients_list).all()) + self.assertDictEqual(quadratic.to_dict(use_name=False), coefficients_dict_int) + self.assertDictEqual(quadratic.to_dict(use_name=True), coefficients_dict_str) + + def test_get_item(self): + """ test get_item. """ + + quadratic_program = QuadraticProgram() + for _ in range(5): + quadratic_program.continuous_var() + + coefficients = [[0 for _ in range(5)] for _ in range(5)] + for i, v in enumerate(coefficients): + for j, _ in enumerate(v): + coefficients[min(i, j)][max(i, j)] += i * j + quadratic = QuadraticExpression(quadratic_program, coefficients) + for i, j_v in enumerate(coefficients): + for j, _ in enumerate(j_v): + if i == j: + self.assertEqual(quadratic[i, j], coefficients[i][j]) + else: + self.assertEqual(quadratic[i, j], coefficients[i][j] + coefficients[j][i]) + + def test_setters(self): + """ test setters. """ + + quadratic_program = QuadraticProgram() + for _ in range(5): + quadratic_program.continuous_var() + + n = quadratic_program.get_num_vars() + zeros = np.zeros((n, n)) + quadratic = QuadraticExpression(quadratic_program, zeros) + + coefficients_list = [[0 for _ in range(5)] for _ in range(5)] + for i, v in enumerate(coefficients_list): + for j, _ in enumerate(v): + coefficients_list[min(i, j)][max(i, j)] += i * j + coefficients_array = np.array(coefficients_list) + coefficients_dok = dok_matrix(coefficients_list) + coefficients_dict_int = {(i, j): v for (i, j), v in coefficients_dok.items()} + coefficients_dict_str = {('x{}'.format(i), 'x{}'.format(j)): v for (i, j), v in + coefficients_dok.items()} + + for coeffs in [coefficients_list, + coefficients_array, + coefficients_dok, + coefficients_dict_int, + coefficients_dict_str]: + quadratic.coefficients = coeffs + self.assertEqual((quadratic.coefficients != coefficients_dok).nnz, 0) + self.assertTrue((quadratic.to_array() == coefficients_list).all()) + self.assertDictEqual(quadratic.to_dict(use_name=False), coefficients_dict_int) + self.assertDictEqual(quadratic.to_dict(use_name=True), coefficients_dict_str) + + def test_evaluate(self): + """ test evaluate. """ + + quadratic_program = QuadraticProgram() + x = [quadratic_program.continuous_var() for _ in range(5)] + + coefficients_list = [[0 for _ in range(5)] for _ in range(5)] + for i, v in enumerate(coefficients_list): + for j, _ in enumerate(v): + coefficients_list[min(i, j)][max(i, j)] += i * j + quadratic = QuadraticExpression(quadratic_program, coefficients_list) + + values_list = list(range(len(x))) + values_array = np.array(values_list) + values_dict_int = {i: i for i in range(len(x))} + values_dict_str = {'x{}'.format(i): i for i in range(len(x))} + + for values in [values_list, values_array, values_dict_int, values_dict_str]: + self.assertEqual(quadratic.evaluate(values), 900) + + def test_symmetric_set(self): + """ test symmetric set """ + q_p = QuadraticProgram() + q_p.binary_var('x') + q_p.binary_var('y') + q_p.binary_var('z') + quad = QuadraticExpression(q_p, {('x', 'y'): -1, ('y', 'x'): 2, ('z', 'x'): 3}) + self.assertDictEqual(quad.to_dict(use_name=True), {('x', 'y'): 1, ('x', 'z'): 3}) + self.assertDictEqual(quad.to_dict(symmetric=True, use_name=True), + {('x', 'y'): 0.5, ('y', 'x'): 0.5, ('x', 'z'): 1.5, ('z', 'x'): 1.5}) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_quadratic_objective.py b/test/optimization/test_quadratic_objective.py new file mode 100644 index 000000000..3babf0f93 --- /dev/null +++ b/test/optimization/test_quadratic_objective.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 QuadraticObjective """ + +import unittest +from test.optimization.optimization_test_case import QiskitOptimizationTestCase + +import numpy as np + +from qiskit.optimization.problems import QuadraticProgram, QuadraticObjective + + +class TestQuadraticObjective(QiskitOptimizationTestCase): + """Test QuadraticObjective""" + + def test_init(self): + """ test init. """ + + quadratic_program = QuadraticProgram() + for _ in range(5): + quadratic_program.continuous_var() + + self.assertEqual(quadratic_program.objective.constant, 0.0) + self.assertEqual(len(quadratic_program.objective.linear.to_dict()), 0) + self.assertEqual(len(quadratic_program.objective.quadratic.to_dict()), 0) + self.assertEqual(quadratic_program.objective.sense, QuadraticObjective.Sense.MINIMIZE) + + constant = 1.0 + linear_coeffs = np.array(range(5)) + lst = [[0 for _ in range(5)] for _ in range(5)] + for i, v in enumerate(lst): + for j, _ in enumerate(v): + lst[min(i, j)][max(i, j)] += i * j + quadratic_coeffs = np.array(lst) + + quadratic_program.minimize(constant, linear_coeffs, quadratic_coeffs) + + self.assertEqual(quadratic_program.objective.constant, constant) + self.assertTrue((quadratic_program.objective.linear.to_array() == linear_coeffs).all()) + self.assertTrue( + (quadratic_program.objective.quadratic.to_array() == quadratic_coeffs).all()) + self.assertEqual(quadratic_program.objective.sense, QuadraticObjective.Sense.MINIMIZE) + + quadratic_program.maximize(constant, linear_coeffs, quadratic_coeffs) + + self.assertEqual(quadratic_program.objective.constant, constant) + self.assertTrue((quadratic_program.objective.linear.to_array() == linear_coeffs).all()) + self.assertTrue( + (quadratic_program.objective.quadratic.to_array() == quadratic_coeffs).all()) + self.assertEqual(quadratic_program.objective.sense, QuadraticObjective.Sense.MAXIMIZE) + + self.assertEqual(quadratic_program.objective.evaluate(linear_coeffs), 931.0) + + def test_setters(self): + """ test setters. """ + + quadratic_program = QuadraticProgram() + for _ in range(5): + quadratic_program.continuous_var() + + constant = 1.0 + linear_coeffs = np.array(range(5)) + lst = [[0 for _ in range(5)] for _ in range(5)] + for i, v in enumerate(lst): + for j, _ in enumerate(v): + lst[min(i, j)][max(i, j)] += i * j + quadratic_coeffs = np.array(lst) + + quadratic_program.objective.constant = constant + quadratic_program.objective.linear = linear_coeffs + quadratic_program.objective.quadratic = quadratic_coeffs + + self.assertEqual(quadratic_program.objective.constant, constant) + self.assertTrue((quadratic_program.objective.linear.to_array() == linear_coeffs).all()) + self.assertTrue( + (quadratic_program.objective.quadratic.to_array() == quadratic_coeffs).all()) + + self.assertEqual(quadratic_program.objective.sense, QuadraticObjective.Sense.MINIMIZE) + + quadratic_program.objective.sense = quadratic_program.objective.Sense.MAXIMIZE + self.assertEqual(quadratic_program.objective.sense, QuadraticObjective.Sense.MAXIMIZE) + + quadratic_program.objective.sense = quadratic_program.objective.Sense.MINIMIZE + self.assertEqual(quadratic_program.objective.sense, QuadraticObjective.Sense.MINIMIZE) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_quadratic_program.py b/test/optimization/test_quadratic_program.py new file mode 100644 index 000000000..85b73d5eb --- /dev/null +++ b/test/optimization/test_quadratic_program.py @@ -0,0 +1,643 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 QuadraticProgram """ + +import unittest +import tempfile +from test.optimization.optimization_test_case import QiskitOptimizationTestCase + +from docplex.mp.model import Model, DOcplexException + +from qiskit.optimization import QuadraticProgram, QiskitOptimizationError, INFINITY +from qiskit.optimization.problems import Variable, Constraint, QuadraticObjective + + +class TestQuadraticProgram(QiskitOptimizationTestCase): + """Test QuadraticProgram without the members that have separate test classes + (VariablesInterface, etc).""" + + def test_constructor(self): + """ test constructor """ + quadratic_program = QuadraticProgram() + self.assertEqual(quadratic_program.name, '') + self.assertEqual(quadratic_program.status, QuadraticProgram.Status.VALID) + self.assertEqual(quadratic_program.get_num_vars(), 0) + self.assertEqual(quadratic_program.get_num_linear_constraints(), 0) + self.assertEqual(quadratic_program.get_num_quadratic_constraints(), 0) + self.assertEqual(quadratic_program.objective.constant, 0) + self.assertDictEqual(quadratic_program.objective.linear.to_dict(), {}) + self.assertDictEqual(quadratic_program.objective.quadratic.to_dict(), {}) + + def test_clear(self): + """ test clear """ + q_p = QuadraticProgram('test') + q_p.binary_var('x') + q_p.binary_var('y') + q_p.minimize(constant=1, linear={'x': 1, 'y': 2}, quadratic={('x', 'x'): 1}) + q_p.linear_constraint({'x': 1}, '==', 1) + q_p.quadratic_constraint({'x': 1}, {('y', 'y'): 2}, '<=', 1) + q_p.clear() + self.assertEqual(q_p.name, '') + self.assertEqual(q_p.status, QuadraticProgram.Status.VALID) + self.assertEqual(q_p.get_num_vars(), 0) + self.assertEqual(q_p.get_num_linear_constraints(), 0) + self.assertEqual(q_p.get_num_quadratic_constraints(), 0) + self.assertEqual(q_p.objective.constant, 0) + self.assertDictEqual(q_p.objective.linear.to_dict(), {}) + self.assertDictEqual(q_p.objective.quadratic.to_dict(), {}) + + def test_name_setter(self): + """ test name setter """ + q_p = QuadraticProgram() + self.assertEqual(q_p.name, '') + name = 'test name' + q_p.name = name + self.assertEqual(q_p.name, name) + + def test_variables_handling(self): + """ test add variables """ + quadratic_program = QuadraticProgram() + + self.assertEqual(quadratic_program.get_num_vars(), 0) + self.assertEqual(quadratic_program.get_num_continuous_vars(), 0) + self.assertEqual(quadratic_program.get_num_binary_vars(), 0) + self.assertEqual(quadratic_program.get_num_integer_vars(), 0) + + x_0 = quadratic_program.continuous_var() + self.assertEqual(x_0.name, 'x0') + self.assertEqual(x_0.lowerbound, 0) + self.assertEqual(x_0.upperbound, INFINITY) + self.assertEqual(x_0.vartype, Variable.Type.CONTINUOUS) + + self.assertEqual(quadratic_program.get_num_vars(), 1) + self.assertEqual(quadratic_program.get_num_continuous_vars(), 1) + self.assertEqual(quadratic_program.get_num_binary_vars(), 0) + self.assertEqual(quadratic_program.get_num_integer_vars(), 0) + + x_1 = quadratic_program.continuous_var(name='x1', lowerbound=5, upperbound=10) + self.assertEqual(x_1.name, 'x1') + self.assertEqual(x_1.lowerbound, 5) + self.assertEqual(x_1.upperbound, 10) + self.assertEqual(x_1.vartype, Variable.Type.CONTINUOUS) + + self.assertEqual(quadratic_program.get_num_vars(), 2) + self.assertEqual(quadratic_program.get_num_continuous_vars(), 2) + self.assertEqual(quadratic_program.get_num_binary_vars(), 0) + self.assertEqual(quadratic_program.get_num_integer_vars(), 0) + + x_2 = quadratic_program.binary_var() + self.assertEqual(x_2.name, 'x2') + self.assertEqual(x_2.lowerbound, 0) + self.assertEqual(x_2.upperbound, 1) + self.assertEqual(x_2.vartype, Variable.Type.BINARY) + + self.assertEqual(quadratic_program.get_num_vars(), 3) + self.assertEqual(quadratic_program.get_num_continuous_vars(), 2) + self.assertEqual(quadratic_program.get_num_binary_vars(), 1) + self.assertEqual(quadratic_program.get_num_integer_vars(), 0) + + x_3 = quadratic_program.binary_var(name='x3') + self.assertEqual(x_3.name, 'x3') + self.assertEqual(x_3.lowerbound, 0) + self.assertEqual(x_3.upperbound, 1) + self.assertEqual(x_3.vartype, Variable.Type.BINARY) + + self.assertEqual(quadratic_program.get_num_vars(), 4) + self.assertEqual(quadratic_program.get_num_continuous_vars(), 2) + self.assertEqual(quadratic_program.get_num_binary_vars(), 2) + self.assertEqual(quadratic_program.get_num_integer_vars(), 0) + + x_4 = quadratic_program.integer_var() + self.assertEqual(x_4.name, 'x4') + self.assertEqual(x_4.lowerbound, 0) + self.assertEqual(x_4.upperbound, INFINITY) + self.assertEqual(x_4.vartype, Variable.Type.INTEGER) + + self.assertEqual(quadratic_program.get_num_vars(), 5) + self.assertEqual(quadratic_program.get_num_continuous_vars(), 2) + self.assertEqual(quadratic_program.get_num_binary_vars(), 2) + self.assertEqual(quadratic_program.get_num_integer_vars(), 1) + + x_5 = quadratic_program.integer_var(name='x5', lowerbound=5, upperbound=10) + self.assertEqual(x_5.name, 'x5') + self.assertEqual(x_5.lowerbound, 5) + self.assertEqual(x_5.upperbound, 10) + self.assertEqual(x_5.vartype, Variable.Type.INTEGER) + + self.assertEqual(quadratic_program.get_num_vars(), 6) + self.assertEqual(quadratic_program.get_num_continuous_vars(), 2) + self.assertEqual(quadratic_program.get_num_binary_vars(), 2) + self.assertEqual(quadratic_program.get_num_integer_vars(), 2) + + with self.assertRaises(QiskitOptimizationError): + quadratic_program.continuous_var(name='x0') + + with self.assertRaises(QiskitOptimizationError): + quadratic_program.binary_var(name='x0') + + with self.assertRaises(QiskitOptimizationError): + quadratic_program.integer_var(name='x0') + + variables = [x_0, x_1, x_2, x_3, x_4, x_5] + for i, x in enumerate(variables): + y = quadratic_program.get_variable(i) + z = quadratic_program.get_variable(x.name) + self.assertEqual(x.name, y.name) + self.assertEqual(x.name, z.name) + self.assertDictEqual(quadratic_program.variables_index, + {'x' + str(i): i for i in range(6)}) + + def test_linear_constraints_handling(self): + """test linear constraints handling""" + q_p = QuadraticProgram() + q_p.binary_var('x') + q_p.binary_var('y') + q_p.binary_var('z') + q_p.linear_constraint({'x': 1}, '==', 1) + q_p.linear_constraint({'y': 1}, '<=', 1) + q_p.linear_constraint({'z': 1}, '>=', 1) + self.assertEqual(q_p.get_num_linear_constraints(), 3) + lin = q_p.linear_constraints + self.assertEqual(len(lin), 3) + + self.assertDictEqual(lin[0].linear.to_dict(), {0: 1}) + self.assertDictEqual(lin[0].linear.to_dict(use_name=True), {'x': 1}) + self.assertListEqual(lin[0].linear.to_array().tolist(), [1, 0, 0]) + self.assertEqual(lin[0].sense, Constraint.Sense.EQ) + self.assertEqual(lin[0].rhs, 1) + self.assertEqual(lin[0].name, 'c0') + self.assertEqual(q_p.get_linear_constraint(0).name, 'c0') + self.assertEqual(q_p.get_linear_constraint('c0').name, 'c0') + + self.assertDictEqual(lin[1].linear.to_dict(), {1: 1}) + self.assertDictEqual(lin[1].linear.to_dict(use_name=True), {'y': 1}) + self.assertListEqual(lin[1].linear.to_array().tolist(), [0, 1, 0]) + self.assertEqual(lin[1].sense, Constraint.Sense.LE) + self.assertEqual(lin[1].rhs, 1) + self.assertEqual(lin[1].name, 'c1') + self.assertEqual(q_p.get_linear_constraint(1).name, 'c1') + self.assertEqual(q_p.get_linear_constraint('c1').name, 'c1') + + self.assertDictEqual(lin[2].linear.to_dict(), {2: 1}) + self.assertDictEqual(lin[2].linear.to_dict(use_name=True), {'z': 1}) + self.assertListEqual(lin[2].linear.to_array().tolist(), [0, 0, 1]) + self.assertEqual(lin[2].sense, Constraint.Sense.GE) + self.assertEqual(lin[2].rhs, 1) + self.assertEqual(lin[2].name, 'c2') + self.assertEqual(q_p.get_linear_constraint(2).name, 'c2') + self.assertEqual(q_p.get_linear_constraint('c2').name, 'c2') + + with self.assertRaises(QiskitOptimizationError): + q_p.linear_constraint(name='c0') + with self.assertRaises(QiskitOptimizationError): + q_p.linear_constraint(name='c1') + with self.assertRaises(QiskitOptimizationError): + q_p.linear_constraint(name='c2') + with self.assertRaises(IndexError): + q_p.get_linear_constraint(4) + with self.assertRaises(KeyError): + q_p.get_linear_constraint('c3') + + q_p.remove_linear_constraint('c1') + lin = q_p.linear_constraints + self.assertEqual(len(lin), 2) + self.assertDictEqual(lin[1].linear.to_dict(), {2: 1}) + self.assertDictEqual(lin[1].linear.to_dict(use_name=True), {'z': 1}) + self.assertListEqual(lin[1].linear.to_array().tolist(), [0, 0, 1]) + self.assertEqual(lin[1].sense, Constraint.Sense.GE) + self.assertEqual(lin[1].rhs, 1) + self.assertEqual(lin[1].name, 'c2') + self.assertEqual(q_p.get_linear_constraint(1).name, 'c2') + self.assertEqual(q_p.get_linear_constraint('c2').name, 'c2') + + with self.assertRaises(KeyError): + q_p.remove_linear_constraint('c1') + with self.assertRaises(IndexError): + q_p.remove_linear_constraint(9) + + q_p.linear_constraint(sense='E') + self.assertEqual(q_p.linear_constraints[-1].sense, Constraint.Sense.EQ) + q_p.linear_constraint(sense='G') + self.assertEqual(q_p.linear_constraints[-1].sense, Constraint.Sense.GE) + q_p.linear_constraint(sense='L') + self.assertEqual(q_p.linear_constraints[-1].sense, Constraint.Sense.LE) + q_p.linear_constraint(sense='EQ') + self.assertEqual(q_p.linear_constraints[-1].sense, Constraint.Sense.EQ) + q_p.linear_constraint(sense='GE') + self.assertEqual(q_p.linear_constraints[-1].sense, Constraint.Sense.GE) + q_p.linear_constraint(sense='LE') + self.assertEqual(q_p.linear_constraints[-1].sense, Constraint.Sense.LE) + q_p.linear_constraint(sense='=') + self.assertEqual(q_p.linear_constraints[-1].sense, Constraint.Sense.EQ) + q_p.linear_constraint(sense='>') + self.assertEqual(q_p.linear_constraints[-1].sense, Constraint.Sense.GE) + q_p.linear_constraint(sense='<') + + with self.assertRaises(QiskitOptimizationError): + q_p.linear_constraint(sense='=>') + + def test_quadratic_constraints_handling(self): + """test quadratic constraints handling""" + q_p = QuadraticProgram() + q_p.binary_var('x') + q_p.binary_var('y') + q_p.binary_var('z') + q_p.quadratic_constraint({'x': 1}, {('x', 'y'): 1}, '==', 1) + q_p.quadratic_constraint({'y': 1}, {('y', 'z'): 1}, '<=', 1) + q_p.quadratic_constraint({'z': 1}, {('z', 'x'): 1}, '>=', 1) + self.assertEqual(q_p.get_num_quadratic_constraints(), 3) + quad = q_p.quadratic_constraints + self.assertEqual(len(quad), 3) + + self.assertDictEqual(quad[0].linear.to_dict(), {0: 1}) + self.assertDictEqual(quad[0].linear.to_dict(use_name=True), {'x': 1}) + self.assertListEqual(quad[0].linear.to_array().tolist(), [1, 0, 0]) + self.assertDictEqual(quad[0].quadratic.to_dict(), {(0, 1): 1}) + self.assertDictEqual(quad[0].quadratic.to_dict(symmetric=True), + {(0, 1): 0.5, (1, 0): 0.5}) + self.assertDictEqual(quad[0].quadratic.to_dict(use_name=True), {('x', 'y'): 1}) + self.assertDictEqual(quad[0].quadratic.to_dict(use_name=True, symmetric=True), + {('x', 'y'): 0.5, ('y', 'x'): 0.5}) + self.assertListEqual(quad[0].quadratic.to_array().tolist(), + [[0, 1, 0], [0, 0, 0], [0, 0, 0]]) + self.assertListEqual(quad[0].quadratic.to_array(symmetric=True).tolist(), + [[0, 0.5, 0], [0.5, 0, 0], [0, 0, 0]]) + self.assertEqual(quad[0].sense, Constraint.Sense.EQ) + self.assertEqual(quad[0].rhs, 1) + self.assertEqual(quad[0].name, 'q0') + self.assertEqual(q_p.get_quadratic_constraint(0).name, 'q0') + self.assertEqual(q_p.get_quadratic_constraint('q0').name, 'q0') + + self.assertDictEqual(quad[1].linear.to_dict(), {1: 1}) + self.assertDictEqual(quad[1].linear.to_dict(use_name=True), {'y': 1}) + self.assertListEqual(quad[1].linear.to_array().tolist(), [0, 1, 0]) + self.assertDictEqual(quad[1].quadratic.to_dict(), {(1, 2): 1}) + self.assertDictEqual(quad[1].quadratic.to_dict(symmetric=True), + {(1, 2): 0.5, (2, 1): 0.5}) + self.assertDictEqual(quad[1].quadratic.to_dict(use_name=True), {('y', 'z'): 1}) + self.assertDictEqual(quad[1].quadratic.to_dict(use_name=True, symmetric=True), + {('y', 'z'): 0.5, ('z', 'y'): 0.5}) + self.assertListEqual(quad[1].quadratic.to_array().tolist(), + [[0, 0, 0], [0, 0, 1], [0, 0, 0]]) + self.assertListEqual(quad[1].quadratic.to_array(symmetric=True).tolist(), + [[0, 0, 0], [0, 0, 0.5], [0, 0.5, 0]]) + self.assertEqual(quad[1].sense, Constraint.Sense.LE) + self.assertEqual(quad[1].rhs, 1) + self.assertEqual(quad[1].name, 'q1') + self.assertEqual(q_p.get_quadratic_constraint(1).name, 'q1') + self.assertEqual(q_p.get_quadratic_constraint('q1').name, 'q1') + + self.assertDictEqual(quad[2].linear.to_dict(), {2: 1}) + self.assertDictEqual(quad[2].linear.to_dict(use_name=True), {'z': 1}) + self.assertListEqual(quad[2].linear.to_array().tolist(), [0, 0, 1]) + self.assertDictEqual(quad[2].quadratic.to_dict(), {(0, 2): 1}) + self.assertDictEqual(quad[2].quadratic.to_dict(symmetric=True), + {(0, 2): 0.5, (2, 0): 0.5}) + self.assertDictEqual(quad[2].quadratic.to_dict(use_name=True), {('x', 'z'): 1}) + self.assertDictEqual(quad[2].quadratic.to_dict(use_name=True, symmetric=True), + {('x', 'z'): 0.5, ('z', 'x'): 0.5}) + self.assertListEqual(quad[2].quadratic.to_array().tolist(), + [[0, 0, 1], [0, 0, 0], [0, 0, 0]]) + self.assertListEqual(quad[2].quadratic.to_array(symmetric=True).tolist(), + [[0, 0, 0.5], [0, 0, 0], [0.5, 0, 0]]) + self.assertEqual(quad[2].sense, Constraint.Sense.GE) + self.assertEqual(quad[2].rhs, 1) + self.assertEqual(quad[2].name, 'q2') + self.assertEqual(q_p.get_quadratic_constraint(2).name, 'q2') + self.assertEqual(q_p.get_quadratic_constraint('q2').name, 'q2') + + with self.assertRaises(QiskitOptimizationError): + q_p.quadratic_constraint(name='q0') + with self.assertRaises(QiskitOptimizationError): + q_p.quadratic_constraint(name='q1') + with self.assertRaises(QiskitOptimizationError): + q_p.quadratic_constraint(name='q2') + with self.assertRaises(IndexError): + q_p.get_quadratic_constraint(4) + with self.assertRaises(KeyError): + q_p.get_quadratic_constraint('q3') + + q_p.remove_quadratic_constraint('q1') + quad = q_p.quadratic_constraints + self.assertEqual(len(quad), 2) + self.assertDictEqual(quad[1].linear.to_dict(), {2: 1}) + self.assertDictEqual(quad[1].linear.to_dict(use_name=True), {'z': 1}) + self.assertListEqual(quad[1].linear.to_array().tolist(), [0, 0, 1]) + self.assertDictEqual(quad[1].quadratic.to_dict(), {(0, 2): 1}) + self.assertDictEqual(quad[1].quadratic.to_dict(symmetric=True), + {(0, 2): 0.5, (2, 0): 0.5}) + self.assertDictEqual(quad[1].quadratic.to_dict(use_name=True), {('x', 'z'): 1}) + self.assertDictEqual(quad[1].quadratic.to_dict(use_name=True, symmetric=True), + {('x', 'z'): 0.5, ('z', 'x'): 0.5}) + self.assertListEqual(quad[1].quadratic.to_array().tolist(), + [[0, 0, 1], [0, 0, 0], [0, 0, 0]]) + self.assertListEqual(quad[1].quadratic.to_array(symmetric=True).tolist(), + [[0, 0, 0.5], [0, 0, 0], [0.5, 0, 0]]) + self.assertEqual(quad[1].sense, Constraint.Sense.GE) + self.assertEqual(quad[1].rhs, 1) + self.assertEqual(quad[1].name, 'q2') + self.assertEqual(q_p.get_quadratic_constraint(1).name, 'q2') + self.assertEqual(q_p.get_quadratic_constraint('q2').name, 'q2') + + with self.assertRaises(KeyError): + q_p.remove_quadratic_constraint('q1') + with self.assertRaises(IndexError): + q_p.remove_quadratic_constraint(9) + + def test_objective_handling(self): + """test objective handling""" + q_p = QuadraticProgram() + q_p.binary_var('x') + q_p.binary_var('y') + q_p.binary_var('z') + q_p.minimize() + obj = q_p.objective + self.assertEqual(obj.sense, QuadraticObjective.Sense.MINIMIZE) + self.assertEqual(obj.constant, 0) + self.assertDictEqual(obj.linear.to_dict(), {}) + self.assertDictEqual(obj.quadratic.to_dict(), {}) + q_p.maximize(1, {'y': 1}, {('z', 'x'): 1, ('y', 'y'): 1}) + obj = q_p.objective + self.assertEqual(obj.sense, QuadraticObjective.Sense.MAXIMIZE) + self.assertEqual(obj.constant, 1) + self.assertDictEqual(obj.linear.to_dict(), {1: 1}) + self.assertDictEqual(obj.linear.to_dict(use_name=True), {'y': 1}) + self.assertListEqual(obj.linear.to_array().tolist(), [0, 1, 0]) + self.assertDictEqual(obj.quadratic.to_dict(), {(0, 2): 1, (1, 1): 1}) + self.assertDictEqual(obj.quadratic.to_dict(symmetric=True), + {(0, 2): 0.5, (2, 0): 0.5, (1, 1): 1}) + self.assertDictEqual(obj.quadratic.to_dict(use_name=True), + {('x', 'z'): 1, ('y', 'y'): 1}) + self.assertDictEqual(obj.quadratic.to_dict(use_name=True, symmetric=True), + {('x', 'z'): 0.5, ('z', 'x'): 0.5, ('y', 'y'): 1}) + self.assertListEqual(obj.quadratic.to_array().tolist(), + [[0, 0, 1], [0, 1, 0], [0, 0, 0]]) + self.assertListEqual(obj.quadratic.to_array(symmetric=True).tolist(), + [[0, 0, 0.5], [0, 1, 0], [0.5, 0, 0]]) + + def test_read_from_lp_file(self): + """test read lp file""" + try: + q_p = QuadraticProgram() + with self.assertRaises(FileNotFoundError): + q_p.read_from_lp_file('') + with self.assertRaises(FileNotFoundError): + q_p.read_from_lp_file('no_file.txt') + q_p.read_from_lp_file('test/optimization/resources/test_quadratic_program.lp') + self.assertEqual(q_p.name, 'my problem') + self.assertEqual(q_p.get_num_vars(), 3) + self.assertEqual(q_p.get_num_binary_vars(), 1) + self.assertEqual(q_p.get_num_integer_vars(), 1) + self.assertEqual(q_p.get_num_continuous_vars(), 1) + self.assertEqual(q_p.get_num_linear_constraints(), 3) + self.assertEqual(q_p.get_num_quadratic_constraints(), 3) + + self.assertEqual(q_p.variables[0].name, 'x') + self.assertEqual(q_p.variables[0].vartype, Variable.Type.BINARY) + self.assertEqual(q_p.variables[0].lowerbound, 0) + self.assertEqual(q_p.variables[0].upperbound, 1) + self.assertEqual(q_p.variables[1].name, 'y') + self.assertEqual(q_p.variables[1].vartype, Variable.Type.INTEGER) + self.assertEqual(q_p.variables[1].lowerbound, -1) + self.assertEqual(q_p.variables[1].upperbound, 5) + self.assertEqual(q_p.variables[2].name, 'z') + self.assertEqual(q_p.variables[2].vartype, Variable.Type.CONTINUOUS) + self.assertEqual(q_p.variables[2].lowerbound, -1) + self.assertEqual(q_p.variables[2].upperbound, 5) + + self.assertEqual(q_p.objective.sense, QuadraticObjective.Sense.MINIMIZE) + self.assertEqual(q_p.objective.constant, 1) + self.assertDictEqual(q_p.objective.linear.to_dict(use_name=True), + {'x': 1, 'y': -1, 'z': 10}) + self.assertDictEqual(q_p.objective.quadratic.to_dict(use_name=True), + {('x', 'x'): 0.5, ('y', 'z'): -1}) + + cst = q_p.linear_constraints + self.assertEqual(cst[0].name, 'lin_eq') + self.assertDictEqual(cst[0].linear.to_dict(use_name=True), {'x': 1, 'y': 2}) + self.assertEqual(cst[0].sense, Constraint.Sense.EQ) + self.assertEqual(cst[0].rhs, 1) + self.assertEqual(cst[1].name, 'lin_leq') + self.assertDictEqual(cst[1].linear.to_dict(use_name=True), {'x': 1, 'y': 2}) + self.assertEqual(cst[1].sense, Constraint.Sense.LE) + self.assertEqual(cst[1].rhs, 1) + self.assertEqual(cst[2].name, 'lin_geq') + self.assertDictEqual(cst[2].linear.to_dict(use_name=True), {'x': 1, 'y': 2}) + self.assertEqual(cst[2].sense, Constraint.Sense.GE) + self.assertEqual(cst[2].rhs, 1) + + cst = q_p.quadratic_constraints + self.assertEqual(cst[0].name, 'quad_eq') + self.assertDictEqual(cst[0].linear.to_dict(use_name=True), {'x': 1, 'y': 1}) + self.assertDictEqual(cst[0].quadratic.to_dict(use_name=True), + {('x', 'x'): 1, ('y', 'z'): -1, ('z', 'z'): 2}) + self.assertEqual(cst[0].sense, Constraint.Sense.EQ) + self.assertEqual(cst[0].rhs, 1) + self.assertEqual(cst[1].name, 'quad_leq') + self.assertDictEqual(cst[1].linear.to_dict(use_name=True), {'x': 1, 'y': 1}) + self.assertDictEqual(cst[1].quadratic.to_dict(use_name=True), + {('x', 'x'): 1, ('y', 'z'): -1, ('z', 'z'): 2}) + self.assertEqual(cst[1].sense, Constraint.Sense.LE) + self.assertEqual(cst[1].rhs, 1) + self.assertEqual(cst[2].name, 'quad_geq') + self.assertDictEqual(cst[2].linear.to_dict(use_name=True), {'x': 1, 'y': 1}) + self.assertDictEqual(cst[2].quadratic.to_dict(use_name=True), + {('x', 'x'): 1, ('y', 'z'): -1, ('z', 'z'): 2}) + self.assertEqual(cst[2].sense, Constraint.Sense.GE) + self.assertEqual(cst[2].rhs, 1) + except RuntimeError as ex: + msg = str(ex) + if 'CPLEX' in msg: + self.skipTest(msg) + else: + self.fail(msg) + + def test_write_to_lp_file(self): + """test write problem""" + q_p = QuadraticProgram('my problem') + q_p.binary_var('x') + q_p.integer_var(-1, 5, 'y') + q_p.continuous_var(-1, 5, 'z') + q_p.minimize(1, {'x': 1, 'y': -1, 'z': 10}, {('x', 'x'): 0.5, ('y', 'z'): -1}) + q_p.linear_constraint({'x': 1, 'y': 2}, '==', 1, 'lin_eq') + q_p.linear_constraint({'x': 1, 'y': 2}, '<=', 1, 'lin_leq') + q_p.linear_constraint({'x': 1, 'y': 2}, '>=', 1, 'lin_geq') + q_p.quadratic_constraint({'x': 1, 'y': 1}, {('x', 'x'): 1, ('y', 'z'): -1, ('z', 'z'): 2}, + '==', 1, 'quad_eq') + q_p.quadratic_constraint({'x': 1, 'y': 1}, {('x', 'x'): 1, ('y', 'z'): -1, ('z', 'z'): 2}, + '<=', 1, 'quad_leq') + q_p.quadratic_constraint({'x': 1, 'y': 1}, {('x', 'x'): 1, ('y', 'z'): -1, ('z', 'z'): 2}, + '>=', 1, 'quad_geq') + + temp_output_file = tempfile.NamedTemporaryFile(mode='w+t', suffix='.lp') + q_p.write_to_lp_file(temp_output_file.name) + with open('test/optimization/resources/test_quadratic_program.lp') as reference: + lines1 = temp_output_file.readlines() + lines2 = reference.readlines() + self.assertListEqual(lines1, lines2) + + temp_output_file.close() # automatically deleted + + with tempfile.TemporaryDirectory() as temp_problem_dir: + q_p.write_to_lp_file(temp_problem_dir) + with open(temp_problem_dir + '/my_problem.lp') as file1, open( + 'test/optimization/resources/test_quadratic_program.lp') as file2: + lines1 = file1.readlines() + lines2 = file2.readlines() + self.assertListEqual(lines1, lines2) + + with self.assertRaises(OSError): + q_p.write_to_lp_file('/cannot/write/this/file.lp') + + with self.assertRaises(DOcplexException): + q_p.write_to_lp_file('') + + def test_docplex(self): + """test from_docplex and to_docplex""" + 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_docplex(q_p.to_docplex()) + self.assertEqual(q_p.pprint_as_string(), q_p2.pprint_as_string()) + self.assertEqual(q_p.export_as_lp_string(), q_p2.export_as_lp_string()) + + mod = Model('test') + x = mod.binary_var('x') + y = mod.integer_var(-2, 4, 'y') + z = mod.continuous_var(-1.5, 3.2, 'z') + mod.minimize(1 + x + 2 * y - x * y + 2 * z * z) + mod.add(2 * x - z == 1, 'c0') + mod.add(2 * x - z + 3 * y * z == 1, 'q0') + self.assertEqual(q_p.pprint_as_string(), mod.pprint_as_string()) + self.assertEqual(q_p.export_as_lp_string(), mod.export_as_lp_string()) + + with self.assertRaises(QiskitOptimizationError): + mod = Model() + mod.semiinteger_var(lb=1, name='x') + q_p.from_docplex(mod) + + with self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var('x') + mod.add_range(0, 2 * x, 1) + q_p.from_docplex(mod) + + with self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var('x') + y = mod.binary_var('y') + mod.add_indicator(x, x + y <= 1, 1) + q_p.from_docplex(mod) + + with self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var('x') + y = mod.binary_var('y') + mod.add_equivalence(x, x + y <= 1, 1) + q_p.from_docplex(mod) + + with self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var('x') + y = mod.binary_var('y') + mod.add(mod.not_equal_constraint(x, y + 1)) + q_p.from_docplex(mod) + + def test_substitute_variables(self): + """test substitute variables""" + 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 = q_p.substitute_variables(constants={'x': -1}) + self.assertEqual(q_p2.status, QuadraticProgram.Status.INFEASIBLE) + q_p2 = q_p.substitute_variables(constants={'y': -3}) + self.assertEqual(q_p2.status, QuadraticProgram.Status.INFEASIBLE) + q_p2 = q_p.substitute_variables(constants={'x': 1, 'z': 2}) + self.assertEqual(q_p2.status, QuadraticProgram.Status.INFEASIBLE) + q_p2.clear() + self.assertEqual(q_p2.status, QuadraticProgram.Status.VALID) + + q_p2 = q_p.substitute_variables(constants={'x': 0}) + self.assertEqual(q_p2.status, QuadraticProgram.Status.VALID) + self.assertDictEqual(q_p2.objective.linear.to_dict(use_name=True), {'y': 2}) + self.assertDictEqual(q_p2.objective.quadratic.to_dict(use_name=True), {('z', 'z'): 2}) + self.assertEqual(q_p2.objective.constant, 1) + self.assertEqual(len(q_p2.linear_constraints), 1) + self.assertEqual(len(q_p2.quadratic_constraints), 1) + + cst = q_p2.linear_constraints[0] + self.assertDictEqual(cst.linear.to_dict(use_name=True), {'z': -1}) + self.assertEqual(cst.sense.name, 'EQ') + self.assertEqual(cst.rhs, 1) + + cst = q_p2.quadratic_constraints[0] + self.assertDictEqual(cst.linear.to_dict(use_name=True), {'z': -1}) + self.assertDictEqual(cst.quadratic.to_dict(use_name=True), {('y', 'z'): 3}) + self.assertEqual(cst.sense.name, 'LE') + self.assertEqual(cst.rhs, -1) + + q_p2 = q_p.substitute_variables(constants={'z': -1}) + self.assertEqual(q_p2.status, QuadraticProgram.Status.VALID) + self.assertDictEqual(q_p2.objective.linear.to_dict(use_name=True), {'x': 1, 'y': 2}) + self.assertDictEqual(q_p2.objective.quadratic.to_dict(use_name=True), {('x', 'y'): -1}) + self.assertEqual(q_p2.objective.constant, 3) + self.assertEqual(len(q_p2.linear_constraints), 2) + self.assertEqual(len(q_p2.quadratic_constraints), 0) + + cst = q_p2.linear_constraints[0] + self.assertDictEqual(cst.linear.to_dict(use_name=True), {'x': 2}) + self.assertEqual(cst.sense.name, 'EQ') + self.assertEqual(cst.rhs, 0) + + cst = q_p2.linear_constraints[1] + self.assertDictEqual(cst.linear.to_dict(use_name=True), {'x': 2, 'y': -3}) + self.assertEqual(cst.sense.name, 'LE') + self.assertEqual(cst.rhs, -2) + + q_p2 = q_p.substitute_variables(variables={'y': ('x', -0.5)}) + self.assertEqual(q_p2.status, QuadraticProgram.Status.VALID) + self.assertDictEqual(q_p2.objective.linear.to_dict(use_name=True), {}) + self.assertDictEqual(q_p2.objective.quadratic.to_dict(use_name=True), + {('x', 'x'): 0.5, ('z', 'z'): 2}) + self.assertEqual(q_p2.objective.constant, 1) + self.assertEqual(len(q_p2.linear_constraints), 1) + self.assertEqual(len(q_p2.quadratic_constraints), 1) + + cst = q_p2.linear_constraints[0] + self.assertDictEqual(cst.linear.to_dict(use_name=True), {'x': 2, 'z': -1}) + self.assertEqual(cst.sense.name, 'EQ') + self.assertEqual(cst.rhs, 1) + + cst = q_p2.quadratic_constraints[0] + self.assertDictEqual(cst.linear.to_dict(use_name=True), {'x': 2, 'z': -1}) + self.assertDictEqual(cst.quadratic.to_dict(use_name=True), {('x', 'z'): -1.5}) + self.assertEqual(cst.sense.name, 'LE') + self.assertEqual(cst.rhs, -1) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_quadratic_program_to_negative_value_oracle.py b/test/optimization/test_quadratic_program_to_negative_value_oracle.py new file mode 100644 index 000000000..678fb5469 --- /dev/null +++ b/test/optimization/test_quadratic_program_to_negative_value_oracle.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 Optimization Problem to Negative Value Oracle.""" + +import unittest +from test.optimization import QiskitOptimizationTestCase +import numpy as np +from qiskit.optimization.algorithms import GroverOptimizer +from qiskit.optimization.converters import QuadraticProgramToNegativeValueOracle +from qiskit import QuantumCircuit, Aer, execute +from qiskit.optimization.problems import QuadraticProgram + + +class TestQuadraticProgramToNegativeValueOracle(QiskitOptimizationTestCase): + """OPtNVO Tests""" + + def _validate_function(self, func_dict, problem): + linear = problem.objective.linear.to_dict() + quadratic = problem.objective.quadratic.to_dict() + for key in func_dict: + if isinstance(key, int) and key >= 0: + self.assertEqual(linear.get(key, 0.0), func_dict[key]) + elif isinstance(key, tuple): + self.assertEqual(quadratic.get((key[0], key[1]), 0.0), func_dict[key]) + else: + self.assertEqual(problem.objective.constant, func_dict[key]) + + def _validate_operator(self, func_dict, n_key, n_value, operator): + + # Get expected results. + solutions = GroverOptimizer._get_qubo_solutions(func_dict, n_key, print_solutions=False) + + # Run the state preparation operator A and observe results. + circuit = operator._circuit + qc = QuantumCircuit() + circuit + hist = self._measure(qc, n_key, n_value) + + # Validate operator A. + for label in hist: + key = int(label[:n_key], 2) + value = self._bin_to_int(label[n_key:n_key + n_value], n_value) + self.assertEqual(int(solutions[key]), value) + + @staticmethod + def _measure(qc, n_key, n_value): + backend = Aer.get_backend('statevector_simulator') + job = execute(qc, backend=backend, shots=1) + result = job.result() + state = np.round(result.get_statevector(qc), 5) + keys = [bin(i)[2::].rjust(int(np.log2(len(state))), '0')[::-1] + for i in range(0, len(state))] + probs = [np.round(abs(a)*abs(a), 5) for a in state] + f_hist = dict(zip(keys, probs)) + hist = {} + for key in f_hist: + new_key = key[:n_key] + key[n_key:n_key+n_value][::-1] + key[n_key+n_value:] + hist[new_key] = f_hist[key] + hist = dict(filter(lambda p: p[1] > 0, hist.items())) + return hist + + @staticmethod + def _bin_to_int(v, num_value_bits): + if v.startswith('1'): + int_v = int(v, 2) - 2 ** num_value_bits + else: + int_v = int(v, 2) + + return int_v + + def test_optnvo_3_linear_2_quadratic_no_constant(self): + """Test with 3 linear coefficients, 2 quadratic, and no constant.""" + try: + # Circuit parameters. + num_value = 4 + + # Input. + problem = QuadraticProgram() + for name in ['x0', 'x1', 'x2']: + problem.binary_var(name) + linear = [-1, 2, -3] + quadratic = {('x0', 'x2'): -2, ('x1', 'x2'): -1} + problem.minimize(linear=linear, quadratic=quadratic) + + # Convert to dictionary format with operator/oracle. + converter = QuadraticProgramToNegativeValueOracle(num_value) + a_operator, _, func_dict = converter.encode(problem) + + self._validate_function(func_dict, problem) + self._validate_operator(func_dict, len(linear), num_value, a_operator) + except NameError as ex: + self.skipTest(str(ex)) + + def test_optnvo_4_key_all_negative(self): + """Test with all negative values.""" + # Circuit parameters. + try: + num_value = 5 + + # Input. + problem = QuadraticProgram() + for name in ['x0', 'x1', 'x2']: + problem.binary_var(name) + linear = [-1, -2, -1] + quadratic = {('x0', 'x1'): -1, ('x0', 'x2'): -2, ('x1', 'x2'): -1} + problem.minimize(constant=-1, linear=linear, quadratic=quadratic) + + # Convert to dictionary format with operator/oracle. + converter = QuadraticProgramToNegativeValueOracle(num_value) + a_operator, _, func_dict = converter.encode(problem) + + self._validate_function(func_dict, problem) + self._validate_operator(func_dict, len(linear), num_value, a_operator) + except NameError as ex: + self.skipTest(str(ex)) + + def test_optnvo_6_key(self): + """Test with 6 linear coefficients, negative quadratics, no constant.""" + # Circuit parameters. + try: + num_value = 4 + + # Input. + problem = QuadraticProgram() + + # Input. + problem = QuadraticProgram() + for name in ['x0', 'x1', 'x2', 'x3', 'x4', 'x5']: + problem.binary_var(name) + linear = [-1, -2, -1, 0, 1, 2] + quadratic = {('x0', 'x3'): -1, ('x1', 'x5'): -2} + problem.minimize(linear=linear, quadratic=quadratic) + + # Convert to dictionary format with operator/oracle. + converter = QuadraticProgramToNegativeValueOracle(num_value) + a_operator, _, func_dict = converter.encode(problem) + + self._validate_function(func_dict, problem) + self._validate_operator(func_dict, len(linear), num_value, a_operator) + except NameError as ex: + self.skipTest(str(ex)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_readme_sample.py b/test/optimization/test_readme_sample.py index 24807bb36..946191f11 100644 --- a/test/optimization/test_readme_sample.py +++ b/test/optimization/test_readme_sample.py @@ -44,8 +44,8 @@ def print(*args): from qiskit.aqua import aqua_globals, QuantumInstance from qiskit.aqua.algorithms import QAOA from qiskit.aqua.components.optimizers import SPSA - from qiskit.optimization.ising import docplex, max_cut - from qiskit.optimization.ising.common import sample_most_likely + from qiskit.optimization.applications.ising import docplex, max_cut + from qiskit.optimization.applications.ising.common import sample_most_likely # Generate a graph of 4 nodes n = 4 diff --git a/test/optimization/test_recursive_optimization.py b/test/optimization/test_recursive_optimization.py new file mode 100755 index 000000000..927660706 --- /dev/null +++ b/test/optimization/test_recursive_optimization.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2020. +# +# 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 Recursive Min Eigen Optimizer.""" + +import unittest +from test.optimization.optimization_test_case import QiskitOptimizationTestCase + +from qiskit.aqua.algorithms import NumPyMinimumEigensolver + +from qiskit.optimization.algorithms import (MinimumEigenOptimizer, CplexOptimizer, + RecursiveMinimumEigenOptimizer) +from qiskit.optimization.problems import QuadraticProgram + + +class TestRecursiveMinEigenOptimizer(QiskitOptimizationTestCase): + """Recursive Min Eigen Optimizer Tests.""" + + def setUp(self): + super().setUp() + self.resource_path = './test/optimization/resources/' + + def test_recursive_min_eigen_optimizer(self): + """Test the recursive minimum eigen optimizer.""" + try: + filename = 'op_ip1.lp' + # get minimum eigen solver + min_eigen_solver = NumPyMinimumEigensolver() + + # construct minimum eigen optimizer + min_eigen_optimizer = MinimumEigenOptimizer(min_eigen_solver) + recursive_min_eigen_optimizer = RecursiveMinimumEigenOptimizer(min_eigen_optimizer, + min_num_vars=4) + + # load optimization problem + problem = QuadraticProgram() + problem.read_from_lp_file(self.resource_path + filename) + + # solve problem with cplex + cplex = CplexOptimizer() + cplex_result = cplex.solve(problem) + + # solve problem + result = recursive_min_eigen_optimizer.solve(problem) + + # analyze results + self.assertAlmostEqual(cplex_result.fval, result.fval) + except RuntimeError as ex: + msg = str(ex) + if 'CPLEX' in msg: + self.skipTest(msg) + else: + self.fail(msg) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_set_packing.py b/test/optimization/test_set_packing.py old mode 100644 new mode 100755 index 1ae3e01f5..0ec5dbbee --- a/test/optimization/test_set_packing.py +++ b/test/optimization/test_set_packing.py @@ -14,12 +14,13 @@ """ Test Set Packing """ +import unittest import json from test.optimization import QiskitOptimizationTestCase import numpy as np -from qiskit.optimization.ising import set_packing -from qiskit.optimization.ising.common import sample_most_likely +from qiskit.optimization.applications.ising import set_packing +from qiskit.optimization.applications.ising.common import sample_most_likely from qiskit.aqua import QuantumInstance, aqua_globals from qiskit.aqua.algorithms import NumPyMinimumEigensolver, VQE from qiskit.aqua.components.optimizers import SPSA @@ -84,3 +85,7 @@ def test_set_packing_vqe(self): ising_sol = set_packing.get_solution(x) oracle = self._brute_force() self.assertEqual(np.count_nonzero(ising_sol), oracle) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_stable_set.py b/test/optimization/test_stable_set.py index 107216288..6379cd1e4 100644 --- a/test/optimization/test_stable_set.py +++ b/test/optimization/test_stable_set.py @@ -14,13 +14,14 @@ """ Test Stable Set """ +import unittest from test.optimization import QiskitOptimizationTestCase import numpy as np from qiskit import BasicAer from qiskit.aqua import aqua_globals, QuantumInstance -from qiskit.optimization.ising import stable_set -from qiskit.optimization.ising.common import random_graph, sample_most_likely +from qiskit.optimization.applications.ising import stable_set +from qiskit.optimization.applications.ising.common import random_graph, sample_most_likely from qiskit.aqua.algorithms import NumPyMinimumEigensolver, VQE from qiskit.aqua.components.optimizers import L_BFGS_B from qiskit.aqua.components.variational_forms import RYRZ @@ -62,3 +63,7 @@ def test_stable_set_vqe(self): ising_sol = stable_set.get_graph_solution(x) np.testing.assert_array_equal(ising_sol, [0, 0, 1, 1, 1]) self.assertEqual(stable_set.stable_set_value(x, self.w), (3.0, False)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_tsp.py b/test/optimization/test_tsp.py index 9c5fe80d2..7d4708437 100644 --- a/test/optimization/test_tsp.py +++ b/test/optimization/test_tsp.py @@ -14,12 +14,13 @@ """ Test TSP (Traveling Salesman Problem) """ +import unittest from test.optimization import QiskitOptimizationTestCase import numpy as np from qiskit.aqua import aqua_globals -from qiskit.optimization.ising import tsp -from qiskit.optimization.ising.common import sample_most_likely +from qiskit.optimization.applications.ising import tsp +from qiskit.optimization.applications.ising.common import sample_most_likely from qiskit.aqua.algorithms import NumPyMinimumEigensolver @@ -41,3 +42,7 @@ def test_tsp(self): x = sample_most_likely(result.eigenstate) order = tsp.get_tsp_solution(x) np.testing.assert_array_equal(order, [1, 2, 0]) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_variable.py b/test/optimization/test_variable.py new file mode 100644 index 000000000..e909a120d --- /dev/null +++ b/test/optimization/test_variable.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 Variable.""" + +import unittest +from test.optimization.optimization_test_case import QiskitOptimizationTestCase + +from qiskit.optimization import INFINITY +from qiskit.optimization.problems import QuadraticProgram, Variable + + +class TestVariable(QiskitOptimizationTestCase): + """Test Variable.""" + + def test_init(self): + """ test init """ + + quadratic_program = QuadraticProgram() + name = 'variable' + lowerbound = 0 + upperbound = 10 + vartype = Variable.Type.INTEGER + + variable = Variable(quadratic_program, name, lowerbound, upperbound, vartype) + + self.assertEqual(variable.name, name) + self.assertEqual(variable.lowerbound, lowerbound) + self.assertEqual(variable.upperbound, upperbound) + self.assertEqual(variable.vartype, Variable.Type.INTEGER) + + def test_init_default(self): + """ test init with default values.""" + + quadratic_program = QuadraticProgram() + name = 'variable' + + variable = Variable(quadratic_program, name) + + self.assertEqual(variable.name, name) + self.assertEqual(variable.lowerbound, 0) + self.assertEqual(variable.upperbound, INFINITY) + self.assertEqual(variable.vartype, Variable.Type.CONTINUOUS) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_vehicle_routing.py b/test/optimization/test_vehicle_routing.py old mode 100644 new mode 100755 index 108b710bd..3486e37b7 --- a/test/optimization/test_vehicle_routing.py +++ b/test/optimization/test_vehicle_routing.py @@ -14,13 +14,14 @@ """ Test Vehicle Routing """ +import unittest from test.optimization import QiskitOptimizationTestCase import numpy as np from qiskit.quantum_info import Pauli from qiskit.aqua import aqua_globals from qiskit.aqua.algorithms import NumPyMinimumEigensolver -from qiskit.optimization.ising.vehicle_routing import get_operator +from qiskit.optimization.applications.ising.vehicle_routing import get_operator # To run only this test, issue: @@ -64,3 +65,7 @@ def test_simple2(self): result = NumPyMinimumEigensolver(self.qubit_op).run() arr = np.array([0., 0., 0., 1.]) np.testing.assert_array_almost_equal(arr, result.eigenstate, 4) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/optimization/test_vertex_cover.py b/test/optimization/test_vertex_cover.py old mode 100644 new mode 100755 index 9d3003e52..0cd9f391c --- a/test/optimization/test_vertex_cover.py +++ b/test/optimization/test_vertex_cover.py @@ -12,15 +12,16 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -""" Text Vertex Cover """ +""" Test Vertex Cover """ +import unittest from test.optimization import QiskitOptimizationTestCase import numpy as np from qiskit import BasicAer from qiskit.aqua import aqua_globals, QuantumInstance -from qiskit.optimization.ising import vertex_cover -from qiskit.optimization.ising.common import random_graph, sample_most_likely +from qiskit.optimization.applications.ising import vertex_cover +from qiskit.optimization.applications.ising.common import random_graph, sample_most_likely from qiskit.aqua.algorithms import NumPyMinimumEigensolver, VQE from qiskit.aqua.components.variational_forms import RYRZ from qiskit.aqua.components.optimizers import SPSA @@ -83,3 +84,7 @@ def test_vertex_cover_vqe(self): sol = vertex_cover.get_graph_solution(x) oracle = self._brute_force() self.assertEqual(np.count_nonzero(sol), oracle) + + +if __name__ == '__main__': + unittest.main()