diff --git a/qiskit/optimization/algorithms/__init__.py b/qiskit/optimization/algorithms/__init__.py index 7384d86652..d1b67e265b 100644 --- a/qiskit/optimization/algorithms/__init__.py +++ b/qiskit/optimization/algorithms/__init__.py @@ -28,34 +28,43 @@ :nosignatures: OptimizationAlgorithm + MultiStartOptimizer -Algorithms -========== +Algorithms and results +====================== .. autosummary:: :toctree: ../stubs/ :nosignatures: + ADMMOptimizationResult ADMMOptimizer + ADMMParameters + ADMMState CobylaOptimizer CplexOptimizer + GroverOptimizationRawResult GroverOptimizer + MinimumEigenOptimizerResult MinimumEigenOptimizer + OptimizationResult + OptimizationResultStatus RecursiveMinimumEigenOptimizer SlsqpOptimizer """ -from .optimization_algorithm import OptimizationResult -from .optimization_algorithm import OptimizationAlgorithm -from .admm_optimizer import ADMMOptimizer -from .cplex_optimizer import CplexOptimizer +from .admm_optimizer import ADMMOptimizer, ADMMOptimizationResult, ADMMState, ADMMParameters from .cobyla_optimizer import CobylaOptimizer -from .minimum_eigen_optimizer import MinimumEigenOptimizer +from .cplex_optimizer import CplexOptimizer +from .grover_optimizer import GroverOptimizer, GroverOptimizationRawResult +from .minimum_eigen_optimizer import MinimumEigenOptimizer, MinimumEigenOptimizerResult +from .multistart_optimizer import MultiStartOptimizer +from .optimization_algorithm import (OptimizationAlgorithm, OptimizationResult, + OptimizationResultStatus) from .recursive_minimum_eigen_optimizer import RecursiveMinimumEigenOptimizer -from .grover_optimizer import GroverOptimizer, GroverOptimizationResults from .slsqp_optimizer import SlsqpOptimizer __all__ = ["ADMMOptimizer", "OptimizationAlgorithm", "OptimizationResult", "CplexOptimizer", "CobylaOptimizer", "MinimumEigenOptimizer", "RecursiveMinimumEigenOptimizer", - "GroverOptimizer", "GroverOptimizationResults", "SlsqpOptimizer"] + "GroverOptimizer", "GroverOptimizationRawResult", "SlsqpOptimizer"] diff --git a/qiskit/optimization/algorithms/admm_optimizer.py b/qiskit/optimization/algorithms/admm_optimizer.py index 434d47ebac..72e3ab1302 100644 --- a/qiskit/optimization/algorithms/admm_optimizer.py +++ b/qiskit/optimization/algorithms/admm_optimizer.py @@ -17,7 +17,7 @@ import logging import time import warnings -from typing import List, Optional, Any, Tuple, cast +from typing import List, Optional, Tuple import numpy as np from qiskit.aqua.algorithms import NumPyMinimumEigensolver @@ -174,19 +174,21 @@ def __init__(self, class ADMMOptimizationResult(OptimizationResult): """ ADMMOptimization Result.""" - def __init__(self, x: Optional[Any] = None, fval: Optional[Any] = None, - state: Optional[ADMMState] = None, results: Optional[Any] = None, - variables: Optional[List[Variable]] = None) -> None: - super().__init__(x=x, - variables=variables, - fval=fval, - results=results or state) - self._state = state + def __init__(self, x: np.ndarray, fval: float, variables: List[Variable], + state: ADMMState) -> None: + """ + Args: + x: the optimal value found by ADMM. + fval: the optimal function value. + variables: the list of variables of the optimization problem. + state: the internal computation state of ADMM. + """ + super().__init__(x=x, fval=fval, variables=variables, raw_results=state) @property - def state(self) -> Optional[ADMMState]: + def state(self) -> ADMMState: """ returns state """ - return self._state + return self._raw_results class ADMMOptimizer(OptimizationAlgorithm): @@ -277,6 +279,7 @@ def solve(self, problem: QuadraticProgram) -> ADMMOptimizationResult: # map integer variables to binary variables from ..converters.integer_to_binary import IntegerToBinary int2bin = IntegerToBinary() + original_variables = problem.variables problem = int2bin.convert(problem) # we deal with minimization in the optimizer, so turn the problem to minimization @@ -363,16 +366,15 @@ def solve(self, problem: QuadraticProgram) -> ADMMOptimizationResult: # flip the objective sign again if required objective_value = objective_value * sense + # convert back integer to binary + base_result = OptimizationResult(solution, objective_value, original_variables) + base_result = int2bin.interpret(base_result) + # third parameter is our internal state of computations. - result = ADMMOptimizationResult(x=solution, - fval=objective_value, - state=self._state, - results={"integer_to_binary_converter": copy.deepcopy( - int2bin)}, - variables=problem.variables) + result = ADMMOptimizationResult(x=base_result.x, fval=base_result.fval, + variables=base_result.variables, + state=self._state) - # convert back integer to binary - result = cast(ADMMOptimizationResult, int2bin.interpret(result)) # debug self._log.debug("solution=%s, objective=%s at iteration=%s", solution, objective_value, iteration) diff --git a/qiskit/optimization/algorithms/cobyla_optimizer.py b/qiskit/optimization/algorithms/cobyla_optimizer.py index 597a047dc8..6513603e69 100644 --- a/qiskit/optimization/algorithms/cobyla_optimizer.py +++ b/qiskit/optimization/algorithms/cobyla_optimizer.py @@ -21,10 +21,10 @@ from .multistart_optimizer import MultiStartOptimizer from .optimization_algorithm import OptimizationResult -from ..problems.quadratic_program import QuadraticProgram -from ..problems.constraint import Constraint from ..exceptions import QiskitOptimizationError from ..infinity import INFINITY +from ..problems.constraint import Constraint +from ..problems.quadratic_program import QuadraticProgram class CobylaOptimizer(MultiStartOptimizer): diff --git a/qiskit/optimization/algorithms/cplex_optimizer.py b/qiskit/optimization/algorithms/cplex_optimizer.py index 5ec6a680cb..209825e96a 100644 --- a/qiskit/optimization/algorithms/cplex_optimizer.py +++ b/qiskit/optimization/algorithms/cplex_optimizer.py @@ -15,8 +15,8 @@ """The CPLEX optimizer wrapped to be used within Qiskit's optimization module.""" -from typing import Optional import logging +from typing import Optional from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult from ..exceptions import QiskitOptimizationError @@ -136,9 +136,9 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult: # create results result = OptimizationResult(x=sol.get_values(), - variables=problem.variables, fval=sol.get_objective_value(), - results=sol) + variables=problem.variables, + raw_results=sol) # return solution return result diff --git a/qiskit/optimization/algorithms/grover_optimizer.py b/qiskit/optimization/algorithms/grover_optimizer.py index 673bac781d..05b3c335da 100644 --- a/qiskit/optimization/algorithms/grover_optimizer.py +++ b/qiskit/optimization/algorithms/grover_optimizer.py @@ -16,21 +16,20 @@ import copy import logging -from typing import Optional, Dict, Union, Tuple import math +from typing import Optional, Dict, Union, Tuple import numpy as np - from qiskit import QuantumCircuit -from qiskit.providers import BaseBackend from qiskit.aqua import QuantumInstance, aqua_globals from qiskit.aqua.algorithms.amplitude_amplifiers.grover import Grover +from qiskit.providers import BaseBackend + from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult -from ..problems.quadratic_program import QuadraticProgram -from ..converters.quadratic_program_to_qubo import QuadraticProgramToQubo from ..converters.quadratic_program_to_negative_value_oracle import \ QuadraticProgramToNegativeValueOracle - +from ..converters.quadratic_program_to_qubo import QuadraticProgramToQubo +from ..problems.quadratic_program import QuadraticProgram logger = logging.getLogger(__name__) @@ -133,7 +132,7 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult: n_value = self._num_value_qubits # Variables for tracking the solutions encountered. - num_solutions = 2**n_key + num_solutions = 2 ** n_key keys_measured = [] # Variables for result object. @@ -143,7 +142,7 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult: # Variables for stopping if we've hit the rotation max. rotations = 0 - max_rotations = int(np.ceil(100*np.pi/4)) + max_rotations = int(np.ceil(100 * np.pi / 4)) # Initialize oracle helper object. orig_constant = problem_.objective.constant @@ -164,7 +163,7 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult: while not improvement_found: # Determine the number of rotations. loops_with_no_improvement += 1 - rotation_count = int(np.ceil(aqua_globals.random.uniform(0, m-1))) + rotation_count = int(np.ceil(aqua_globals.random.uniform(0, m - 1))) rotations += rotation_count # Apply Grover's Algorithm to find values below the threshold. @@ -196,7 +195,7 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult: threshold = optimum_value else: # Using Durr and Hoyer method, increase m. - m = int(np.ceil(min(m * 8/7, 2**(n_key / 2)))) + 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. @@ -225,16 +224,17 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult: 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) + grover_results = GroverOptimizationRawResult(operation_count, n_key, n_value, func_dict) fval = solutions[optimum_key] if sense == problem_.objective.Sense.MAXIMIZE: fval = -fval - result = OptimizationResult(x=opt_x, variables=problem.variables, fval=fval, - results={"grover_results": grover_results, - "qubo_converter": copy.deepcopy(self._qubo_converter), - "negative_value_oracle_converter": copy.deepcopy( - opt_prob_converter) - }) + result = OptimizationResult(x=opt_x, fval=fval, variables=problem.variables, + raw_results={"grover_results": grover_results, + "qubo_converter": copy.deepcopy( + self._qubo_converter), + "negative_value_oracle_converter": copy.deepcopy( + opt_prob_converter) + }) # cast binaries back to integers result = self._qubo_converter.interpret(result) @@ -247,7 +247,7 @@ def _measure(self, circuit: QuantumCircuit) -> str: 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.0 - sum([x[1] for x in freq[0:len(freq)-1]])) + freq[-1] = (freq[-1][0], 1.0 - sum(x[1] for x in freq[0:len(freq) - 1])) idx = aqua_globals.random.choice(len(freq), 1, p=[x[1] for x in freq])[0] logger.info('Frequencies: %s', freq) @@ -261,7 +261,7 @@ def _get_probs(self, qc: QuantumCircuit) -> Dict[str, float]: 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] + probs = [np.round(abs(a) * abs(a), 5) for a in state] hist = dict(zip(keys, probs)) else: state = result.get_counts(qc) @@ -276,13 +276,13 @@ def _get_probs(self, qc: QuantumCircuit) -> Dict[str, float]: @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 + assert -2 ** n_bits <= v < 2 ** n_bits if v < 0: - v += 2**n_bits + v += 2 ** n_bits bin_v = bin(v)[2:] else: - format_string = '{0:0'+str(n_bits)+'b}' + format_string = '{0:0' + str(n_bits) + 'b}' bin_v = format_string.format(v) return bin_v @@ -315,14 +315,14 @@ def _get_qubo_solutions(function_dict: Dict[Union[int, Tuple[int, int]], int], n constant = 0 if -1 in function_dict: constant = function_dict[-1] - format_string = '{0:0'+str(n_key)+'b}' + 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): + for i in range(2 ** n_key): solution = constant # Convert int to a list of binary variables. @@ -355,8 +355,8 @@ def _get_qubo_solutions(function_dict: Dict[Union[int, Tuple[int, int]], int], n return solutions -class GroverOptimizationResults: - """A results object for Grover Optimization methods.""" +class GroverOptimizationRawResult: + """A raw result object for Grover Optimization methods.""" def __init__(self, operation_counts: Dict[int, Dict[str, int]], n_input_qubits: int, n_output_qubits: int, diff --git a/qiskit/optimization/algorithms/minimum_eigen_optimizer.py b/qiskit/optimization/algorithms/minimum_eigen_optimizer.py index 01f29e2baf..fab23e9054 100644 --- a/qiskit/optimization/algorithms/minimum_eigen_optimizer.py +++ b/qiskit/optimization/algorithms/minimum_eigen_optimizer.py @@ -1,4 +1,3 @@ - # -*- coding: utf-8 -*- # This code is part of Qiskit. @@ -14,39 +13,41 @@ # that they have been altered from the originals. """A wrapper for minimum eigen solvers from Aqua to be used within the optimization module.""" -import copy -from typing import Optional, Any, Union, Tuple, List, cast -import numpy as np +from copy import deepcopy +from typing import Optional, Any, Union, Tuple, List +import numpy as np from qiskit.aqua.algorithms import MinimumEigensolver from qiskit.aqua.operators import StateFn, DictStateFn from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult -from ..problems.quadratic_program import QuadraticProgram -from ..problems.variable import Variable from ..converters.quadratic_program_to_qubo import QuadraticProgramToQubo +from ..problems.quadratic_program import QuadraticProgram, Variable 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, - variables: Optional[List[Variable]] = None) -> None: - super().__init__(x, fval, results, variables=variables) - self._samples = samples + def __init__(self, x: List[float], fval: float, variables: List[Variable], + samples: List[Tuple[str, float, float]], + qubo_converter: QuadraticProgramToQubo) -> None: + """ + Args: + x: the optimal value found by ``MinimumEigensolver``. + fval: the optimal function value. + variables: the list of variables of the optimization problem. + samples: the basis state as bitstring, the QUBO value, and the probability of sampling. + qubo_converter: ``QuadraticProgram`` to QUBO converter. + """ + super().__init__(x=x, fval=fval, variables=variables, + raw_results={'samples': samples, 'qubo_converter': qubo_converter}) @property - def samples(self) -> Any: + def samples(self) -> List[Tuple[str, float, float]]: """ returns samples """ - return self._samples + return self._raw_results['samples'] - @samples.setter - def samples(self, samples: Any) -> None: - """ set samples """ - self._samples = samples - - def get_correlations(self): + def get_correlations(self) -> np.ndarray: """ get correlation matrix from samples """ states = [v[0] for v in self.samples] @@ -161,24 +162,23 @@ def solve(self, problem: QuadraticProgram) -> MinimumEigenOptimizerResult: # 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]) - x = samples[0][0] + x = [float(e) for e in samples[0][0]] fval = samples[0][1] # if Hamiltonian is empty, then the objective function is constant to the offset else: - x = [0]*problem_.get_num_binary_vars() + x = [0] * problem_.get_num_binary_vars() fval = offset - x_str = '0'*problem_.get_num_binary_vars() + x_str = '0' * problem_.get_num_binary_vars() samples = [(x_str, offset, 1.0)] # translate result back to integers - opt_res = MinimumEigenOptimizerResult(x=x, fval=fval, samples=samples, - results={"qubo_converter": copy.deepcopy( - self._qubo_converter)}, - variables=problem.variables) - opt_res = cast(MinimumEigenOptimizerResult, self._qubo_converter.interpret(opt_res)) - - # translate results back to original problem + base_res = OptimizationResult(x=x, fval=fval, variables=problem.variables) + base_res = self._qubo_converter.interpret(base_res) + opt_res = MinimumEigenOptimizerResult(x=base_res.x, fval=base_res.fval, + variables=base_res.variables, + samples=samples, + qubo_converter=deepcopy(self._qubo_converter)) return opt_res @@ -215,7 +215,7 @@ def _eigenvector_to_solutions(eigenvector: Union[dict, np.ndarray, StateFn], TypeError: If the type of eigenvector is not supported. """ if isinstance(eigenvector, DictStateFn): - eigenvector = {bitstr: val**2 for (bitstr, val) in eigenvector.primitive.items()} + eigenvector = {bitstr: val ** 2 for (bitstr, val) in eigenvector.primitive.items()} elif isinstance(eigenvector, StateFn): eigenvector = eigenvector.to_matrix() diff --git a/qiskit/optimization/algorithms/multistart_optimizer.py b/qiskit/optimization/algorithms/multistart_optimizer.py index c14bfb7a1b..73e9b65a19 100644 --- a/qiskit/optimization/algorithms/multistart_optimizer.py +++ b/qiskit/optimization/algorithms/multistart_optimizer.py @@ -25,7 +25,8 @@ from scipy.stats import uniform from qiskit.optimization import QuadraticProgram, INFINITY -from qiskit.optimization.algorithms import OptimizationAlgorithm, OptimizationResult +from qiskit.optimization.algorithms.optimization_algorithm import (OptimizationAlgorithm, + OptimizationResult) logger = logging.getLogger(__name__) @@ -91,7 +92,8 @@ def multi_start_solve(self, minimize: Callable[[np.array], np.array], fval_sol = fval * problem.objective.sense.value x_sol = x - return OptimizationResult(x_sol, fval_sol, x_sol, variables=problem.variables) + return OptimizationResult(x=x_sol, fval=fval_sol, variables=problem.variables, + raw_results=x_sol) @property def trials(self) -> int: diff --git a/qiskit/optimization/algorithms/optimization_algorithm.py b/qiskit/optimization/algorithms/optimization_algorithm.py index 219909f2cb..9b14044ae1 100644 --- a/qiskit/optimization/algorithms/optimization_algorithm.py +++ b/qiskit/optimization/algorithms/optimization_algorithm.py @@ -16,7 +16,9 @@ from abc import ABC, abstractmethod from enum import Enum -from typing import Any, Optional, Union, List, Dict +from typing import List, Union, Any, Optional, Dict + +import numpy as np from .. import QiskitOptimizationError from ..problems.quadratic_program import QuadraticProgram, Variable @@ -86,74 +88,113 @@ def _verify_compatibility(self, problem: QuadraticProgram) -> None: class OptimizationResultStatus(Enum): - """Feasible values for the termination status of an optimization algorithm.""" + """Termination status of an optimization algorithm.""" + SUCCESS = 0 + """the optimization algorithm succeeded to find an optimal solution.""" + FAILURE = 1 + """the optimization algorithm ended in a failure.""" + INFEASIBLE = 2 + """the optimization algorithm obtained an infeasible solution.""" 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. - variables: The list of variables under optimization. + The optimization algorithms return an object of the type ``OptimizationResult`` + with the information about the solution obtained. + + ``OptimizationResult`` allows users to get the value of a variable by specifying an index or + a name as follows. + + Examples: + >>> from qiskit.optimization import QuadraticProgram + >>> from qiskit.optimization.algorithms import CplexOptimizer + >>> problem = QuadraticProgram() + >>> _ = problem.binary_var('x1') + >>> _ = problem.binary_var('x2') + >>> _ = problem.binary_var('x3') + >>> problem.minimize(linear={'x1': 1, 'x2': -2, 'x3': 3}) + >>> print([var.name for var in problem.variables]) + ['x1', 'x2', 'x3'] + >>> optimizer = CplexOptimizer() + >>> result = optimizer.solve(problem) + >>> print(result.variable_names) + ['x1', 'x2', 'x3'] + >>> print(result.x) + [0. 1. 0.] + >>> print(result[1]) + 1.0 + >>> print(result['x1']) + 0.0 + >>> print(result.fval) + -2.0 + >>> print(result.variables_dict) + {'x1': 0.0, 'x2': 1.0, 'x3': 0.0} + + Note: + The order of variables should be equal to that of the problem solved by + optimization algorithms. Optimization algorithms and converters of ``QuadraticProgram`` + should maintain the order when generating a new ``OptimizationResult`` object. """ - Status = OptimizationResultStatus - - def __init__(self, x: Optional[Any] = None, fval: Optional[Any] = None, - results: Optional[Any] = None, - status: OptimizationResultStatus = OptimizationResultStatus.SUCCESS, - variables: Optional[List[Variable]] = None) -> None: - self._x = x if x is not None else [] # pylint: disable=invalid-name - self._variables = variables if variables is not None else [] + def __init__(self, x: Union[List[float], np.ndarray], fval: float, + variables: List[Variable], + raw_results: Optional[Any] = None, + status: OptimizationResultStatus = OptimizationResultStatus.SUCCESS) -> None: + """ + Args: + x: the optimal value found in the optimization. + fval: the optimal function value. + variables: the list of variables of the optimization problem. + raw_results: the original results object from the optimization algorithm. + status: the termination status of the optimization algorithm. + """ + self._x = x if isinstance(x, np.ndarray) else np.array(x) # pylint: disable=invalid-name self._fval = fval - self._results = results + self._raw_results = raw_results self._status = status - self._variable_names = [variable.name for variable in self._variables] + self._variables = variables + self._variable_names = [var.name for var in self._variables] self._variables_dict = dict(zip(self._variable_names, self._x)) - def __repr__(self): - self._x = self._x if self._x is not None else [] - return 'optimal variables: [{}]\noptimal function value: {}\nstatus: {}' \ - .format(','.join([str(x_) for x_ in self._x]), self._fval, self._status.name) + def __repr__(self) -> str: + return 'optimal function value: {}\n' \ + 'optimal value: {}\n' \ + 'status: {}'.format(self._fval, self._x, self._status.name) - def __getitem__(self, item: Union[int, str]): - if isinstance(item, int): - return self._x[item] - if isinstance(item, str): - return self._variables_dict[item] - raise QiskitOptimizationError("Integer or string parameter required, instead " - + type(item).__name__ + " provided.") + def __getitem__(self, key: Union[int, str]) -> float: + """Returns the value of the variable whose index or name is equal to ``key``. - @property - def variables_dict(self) -> Optional[Dict[str, int]]: - """Returns the pairs of variable names and values under optimization. - - Returns: - The pairs of variable names and values under optimization. - """ - return self._variables_dict + The key can be an integer or a string. + If the key is an integer, this methods returns the value of the variable + whose index is equal to ``key``. + If the key is a string, this methods return the value of the variable + whose name is equal to ``key``. - @property - def variable_names(self) -> Optional[List[str]]: - """Returns the list of variable names under optimization. + Args: + key: an integer or a string. Returns: - The list of variable names under optimization. + The value of a variable whose index or name is equal to ``key``. + + Raises: + IndexError: if ``key`` is an integer and is out of range of the variables. + KeyError: if ``key`` is a string and none of the variables has ``key`` as name. + TypeError: if ``key`` is neither an integer nor a string. """ - return self._variable_names + if isinstance(key, int): + return self._x[key] + if isinstance(key, str): + return self._variables_dict[key] + raise TypeError( + "Integer or string key required," + "instead {}({}) provided.".format(type(key), key)) @property - def x(self) -> Any: + def x(self) -> np.ndarray: """Returns the optimal value found in the optimization. Returns: @@ -161,17 +202,8 @@ def x(self) -> Any: """ return self._x - @x.setter # type: ignore - def x(self, x: Any) -> None: - """Set a new optimal value. - - Args: - x: The new optimal value. - """ - self._x = x - @property - def fval(self) -> Any: + def fval(self) -> float: """Returns the optimal function value. Returns: @@ -179,49 +211,49 @@ def fval(self) -> Any: """ return self._fval - @fval.setter # type: ignore - def fval(self, fval: Any) -> None: - """Set a new optimal function value. - - Args: - fval: The new optimal function value. - """ - self._fval = fval - @property - def results(self) -> Any: - """Return the original results object from the algorithm. + def raw_results(self) -> Any: + """Return the original results object from the optimization algorithm. Currently a dump for any leftovers. Returns: Additional result information of the optimization algorithm. """ - return self._results - - @results.setter # type: ignore - def results(self, results: Any) -> None: - """Set results. - - Args: - results: The new additional results of the optimization. - """ - self._results = results + return self._raw_results @property def status(self) -> OptimizationResultStatus: - """Return the termination status of the algorithm. + """Returns the termination status of the optimization algorithm. Returns: The termination status of the algorithm. """ return self._status - @status.setter # type: ignore - def status(self, status: OptimizationResultStatus) -> None: - """Set a new termination status. + @property + def variables(self) -> List[Variable]: + """Returns the list of variables of the optimization problem. - Args: - status: The new termination status. + Returns: + The list of variables. """ - self._status = status + return self._variables + + @property + def variables_dict(self) -> Dict[str, float]: + """Returns the optimal value as a dictionary of the variable name and corresponding value. + + Returns: + The optimal value as a dictionary of the variable name and corresponding value. + """ + return self._variables_dict + + @property + def variable_names(self) -> List[str]: + """Returns the list of variable names of the optimization problem. + + Returns: + The list of variable names of the optimization problem. + """ + return self._variable_names diff --git a/qiskit/optimization/algorithms/recursive_minimum_eigen_optimizer.py b/qiskit/optimization/algorithms/recursive_minimum_eigen_optimizer.py index 3c3038a1ac..ff66a61a56 100644 --- a/qiskit/optimization/algorithms/recursive_minimum_eigen_optimizer.py +++ b/qiskit/optimization/algorithms/recursive_minimum_eigen_optimizer.py @@ -14,19 +14,19 @@ """A recursive minimal eigen optimizer in Qiskit's optimization module.""" +import logging from copy import deepcopy from typing import Optional -import logging -import numpy as np +import numpy as np from qiskit.aqua.algorithms import NumPyMinimumEigensolver from qiskit.aqua.utils.validation import validate_min +from .minimum_eigen_optimizer import MinimumEigenOptimizer, MinimumEigenOptimizerResult from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult -from .minimum_eigen_optimizer import MinimumEigenOptimizer +from ..converters.quadratic_program_to_qubo import QuadraticProgramToQubo from ..exceptions import QiskitOptimizationError from ..problems.quadratic_program import QuadraticProgram -from ..converters.quadratic_program_to_qubo import QuadraticProgramToQubo logger = logging.getLogger(__name__) @@ -133,7 +133,7 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult: while problem_.get_num_vars() > self._min_num_vars: # solve current problem with optimizer - res = self._min_eigen_optimizer.solve(problem_) + res = self._min_eigen_optimizer.solve(problem_) # type: MinimumEigenOptimizerResult # analyze results to get strongest correlation correlations = res.get_correlations() @@ -210,7 +210,7 @@ def find_value(x, replacements, var_values): x_v = [var_values[x_aux.name] for x_aux in problem_ref.variables] fval = result.fval results = OptimizationResult(x=x_v, fval=fval, - results=(replacements, deepcopy(self._qubo_converter)), + raw_results=(replacements, deepcopy(self._qubo_converter)), variables=problem.variables) results = self._qubo_converter.interpret(results) return results diff --git a/qiskit/optimization/converters/inequality_to_equality.py b/qiskit/optimization/converters/inequality_to_equality.py index 0e8ee76147..a03ca124da 100644 --- a/qiskit/optimization/converters/inequality_to_equality.py +++ b/qiskit/optimization/converters/inequality_to_equality.py @@ -18,13 +18,13 @@ import math from typing import List, Optional, Union +from .quadratic_program_converter import QuadraticProgramConverter from ..algorithms.optimization_algorithm import OptimizationResult from ..exceptions import QiskitOptimizationError from ..problems.constraint import Constraint from ..problems.quadratic_objective import QuadraticObjective from ..problems.quadratic_program import QuadraticProgram from ..problems.variable import Variable -from .quadratic_program_converter import QuadraticProgramConverter logger = logging.getLogger(__name__) @@ -171,9 +171,10 @@ def convert(self, problem: QuadraticProgram) -> QuadraticProgram: def _add_integer_slack_var_linear_constraint(self, linear, sense, rhs, name): # If a coefficient that is not integer exist, raise error - if self._contains_any_float_value(list(linear.values())): - raise QiskitOptimizationError(name + ' contains float coefficients. ' - 'We can not use an integer slack variable for ' + name) + if self._contains_any_float_value(linear.values()): + raise QiskitOptimizationError( + '"{0}" contains float coefficients. ' + 'We can not use an integer slack variable for "{0}"'.format(name)) # If rhs is float number, round up/down to the nearest integer. new_rhs = rhs @@ -240,8 +241,9 @@ def _add_integer_slack_var_quadratic_constraint(self, linear, quadratic, sense, # If a coefficient that is not integer exist, raise an error if (self._contains_any_float_value(list(linear.values())) or self._contains_any_float_value(list(quadratic.values()))): - raise QiskitOptimizationError(name + ' contains float coefficients. ' - 'We can not use an integer slack variable for ' + name) + raise QiskitOptimizationError( + '"{0}" contains float coefficients. ' + 'We can not use an integer slack variable for "{0}"'.format(name)) # If rhs is float number, round up/down to the nearest integer. new_rhs = rhs @@ -348,13 +350,11 @@ def interpret(self, result: OptimizationResult) -> OptimizationResult: Returns: The result of the original problem. """ - # copy fval and status of the result of the converted problem - new_result = copy.deepcopy(result) # convert back the optimization result into that of the original problem names = [x.name for x in self._dst.variables] - vals = result.x - new_result.x = self._interpret_var(names, vals) # type: ignore - return new_result + new_x = self._interpret_var(names, result.x) + return OptimizationResult(x=new_x, fval=result.fval, variables=self._dst.variables, + raw_results=result.raw_results, status=result.status) def _interpret_var(self, names, vals) -> List[int]: # interpret slack variables diff --git a/qiskit/optimization/converters/integer_to_binary.py b/qiskit/optimization/converters/integer_to_binary.py index dab9352b62..3bf5ad8734 100644 --- a/qiskit/optimization/converters/integer_to_binary.py +++ b/qiskit/optimization/converters/integer_to_binary.py @@ -16,7 +16,7 @@ import copy import logging -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple, Union import numpy as np @@ -55,7 +55,6 @@ def __init__(self) -> None: self._src = None # type: Optional[QuadraticProgram] self._dst = None # type: Optional[QuadraticProgram] self._conv = {} # type: Dict[Variable, List[Tuple[str, int]]] - # e.g., self._conv = {x: [('x@1', 1), ('x@2', 2)]} def convert(self, problem: QuadraticProgram) -> QuadraticProgram: @@ -220,17 +219,13 @@ def interpret(self, result: OptimizationResult) -> OptimizationResult: Returns: The result of the original problem. """ - # copy fval and status of the result of the converted problem - new_result = copy.deepcopy(result) - # convert back additional binary variables into integer variables - vals = result.x - new_result.x = self._interpret_var(vals) # type: ignore - - return new_result + new_x = self._interpret_var(result.x) + return OptimizationResult(x=new_x, fval=result.fval, variables=result.variables, + raw_results=result.raw_results) - def _interpret_var(self, vals) -> List[float]: + def _interpret_var(self, vals: Union[List[float], np.ndarray]) -> List[float]: # interpret integer values - sol = {x.name: float(vals[i]) for i, x in enumerate(self._dst.variables)} + sol = {x.name: vals[i] for i, x in enumerate(self._dst.variables)} new_vals = [] for x in self._src.variables: if x in self._conv: diff --git a/qiskit/optimization/converters/linear_equality_to_penalty.py b/qiskit/optimization/converters/linear_equality_to_penalty.py index edf9e20d81..d6d6dbc965 100644 --- a/qiskit/optimization/converters/linear_equality_to_penalty.py +++ b/qiskit/optimization/converters/linear_equality_to_penalty.py @@ -15,16 +15,16 @@ """Converter to convert a problem with equality constraints to unconstrained with penalty terms.""" import copy -from typing import Optional, cast, Union, Tuple, Dict -from math import fsum import logging +from math import fsum +from typing import Optional, cast, Union, Tuple, Dict from ..algorithms.optimization_algorithm import OptimizationResult, OptimizationResultStatus -from ..problems.quadratic_program import QuadraticProgram, QuadraticProgramStatus -from ..problems.variable import Variable +from ..exceptions import QiskitOptimizationError from ..problems.constraint import Constraint from ..problems.quadratic_objective import QuadraticObjective -from ..exceptions import QiskitOptimizationError +from ..problems.quadratic_program import QuadraticProgram, QuadraticProgramStatus +from ..problems.variable import Variable from .quadratic_program_converter import QuadraticProgramConverter logger = logging.getLogger(__name__) @@ -185,19 +185,15 @@ def interpret(self, result: OptimizationResult) -> OptimizationResult: substitute_dict[variables[i].name] = float(result.x[i]) substituted_qp = self._src.substitute_variables(substitute_dict) - new_result = copy.deepcopy(result) - new_result.x = result.x - - # Set the new function value - new_result.fval = substituted_qp.objective.constant - # Set the new status of optimization result if substituted_qp.status == QuadraticProgramStatus.VALID: - new_result.status = OptimizationResultStatus.SUCCESS + new_status = OptimizationResultStatus.SUCCESS else: - new_result.status = OptimizationResultStatus.INFEASIBLE + new_status = OptimizationResultStatus.INFEASIBLE - return new_result + return OptimizationResult(x=result.x, fval=substituted_qp.objective.constant, + variables=result.variables, raw_results=result.raw_results, + status=new_status) @property def penalty(self) -> Optional[float]: diff --git a/releasenotes/notes/opt-result-update-cdb157f6a67483bb.yaml b/releasenotes/notes/opt-result-update-cdb157f6a67483bb.yaml new file mode 100644 index 0000000000..85ae9b1f47 --- /dev/null +++ b/releasenotes/notes/opt-result-update-cdb157f6a67483bb.yaml @@ -0,0 +1,11 @@ +fixes: + - | + ``OptimizationResult`` included some public setters and class variables + were ``Optional``. This fix makes all class variables read-only so that + mypy and pylint can check types more effectively. + ``MinimumEigenOptimizer.solve`` generated bitstrings in a result as ``str``. + This fix changed the result into ``List[float]`` as the other algorithms do. + Some public classes related to optimization algorithms were missing in + the documentation of ``qiskit.optimization.algorithms``. This fix added + all such classes to the docstring. + `#1131 ` for more details. diff --git a/test/optimization/test_converters.py b/test/optimization/test_converters.py index b30a531166..9fc39a5a0d 100644 --- a/test/optimization/test_converters.py +++ b/test/optimization/test_converters.py @@ -400,10 +400,10 @@ def test_binary_to_integer(self): linear[x.name] = 1 op.linear_constraint(linear, Constraint.Sense.EQ, 6, 'x0x1x2') conv = IntegerToBinary() - _ = conv.convert(op) - result = OptimizationResult(x=[0, 1, 1, 1, 1], fval=17) + op2 = conv.convert(op) + result = OptimizationResult(x=[0, 1, 1, 1, 1], fval=17, variables=op2.variables) new_result = conv.interpret(result) - self.assertListEqual(new_result.x, [0, 1, 5]) + np.testing.assert_array_almost_equal(new_result.x, [0, 1, 5]) self.assertEqual(new_result.fval, 17) def test_optimizationproblem_to_ising(self): @@ -532,7 +532,7 @@ def test_auto_penalty(self): result = exact.solve(qubo) result_auto = exact.solve(qubo_auto) self.assertEqual(result.fval, result_auto.fval) - self.assertListEqual(result.x, result_auto.x) + np.testing.assert_array_almost_equal(result.x, result_auto.x) def test_auto_penalty_warning(self): """ Test warnings of auto penalty function""" @@ -556,11 +556,11 @@ def test_auto_penalty_warning(self): def test_linear_equality_to_penalty_decode(self): """ Test decode func of LinearEqualityToPenalty""" qprog = QuadraticProgram() - qprog .binary_var('x') - qprog .binary_var('y') - qprog .binary_var('z') - qprog .maximize(linear={'x': 3, 'y': 1, 'z': 1}) - qprog .linear_constraint(linear={'x': 1, 'y': 1, 'z': 1}, sense='EQ', rhs=2, name='xyz_eq') + qprog.binary_var('x') + qprog.binary_var('y') + qprog.binary_var('z') + qprog.maximize(linear={'x': 3, 'y': 1, 'z': 1}) + qprog.linear_constraint(linear={'x': 1, 'y': 1, 'z': 1}, sense='EQ', rhs=2, name='xyz_eq') lineq2penalty = LinearEqualityToPenalty() qubo = lineq2penalty.convert(qprog) exact_mes = NumPyMinimumEigensolver() @@ -568,12 +568,12 @@ def test_linear_equality_to_penalty_decode(self): result = exact.solve(qubo) decoded_result = lineq2penalty.interpret(result) self.assertEqual(decoded_result.fval, 4) - self.assertListEqual(decoded_result.x, [1, 1, 0]) + np.testing.assert_array_almost_equal(decoded_result.x, [1, 1, 0]) self.assertEqual(decoded_result.status, OptimizationResultStatus.SUCCESS) - infeasible_result = OptimizationResult(x=[1, 1, 1]) + infeasible_result = OptimizationResult(x=[1, 1, 1], fval=0, variables=qprog.variables) decoded_infeasible_result = lineq2penalty.interpret(infeasible_result) self.assertEqual(decoded_infeasible_result.fval, 5) - self.assertListEqual(decoded_infeasible_result.x, [1, 1, 1]) + np.testing.assert_array_almost_equal(decoded_infeasible_result.x, [1, 1, 1]) self.assertEqual(decoded_infeasible_result.status, OptimizationResultStatus.INFEASIBLE) diff --git a/test/optimization/test_grover_optimizer.py b/test/optimization/test_grover_optimizer.py index 9a50dd6178..9736a5b51f 100644 --- a/test/optimization/test_grover_optimizer.py +++ b/test/optimization/test_grover_optimizer.py @@ -17,6 +17,7 @@ import unittest from test.optimization import QiskitOptimizationTestCase from docplex.mp.model import Model +import numpy as np from qiskit import Aer from qiskit.aqua import aqua_globals, QuantumInstance from qiskit.aqua.algorithms import NumPyMinimumEigensolver @@ -40,8 +41,8 @@ def validate_results(self, problem, results): comp_result = solver.solve(problem) # Validate results. - self.assertTrue(comp_result.x == results.x) - self.assertTrue(comp_result.fval == results.fval) + np.testing.assert_array_almost_equal(comp_result.x, results.x) + self.assertEqual(comp_result.fval, results.fval) def test_qubo_gas_int_zero(self): """Test for when the answer is zero.""" @@ -57,7 +58,7 @@ def test_qubo_gas_int_zero(self): # 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]) + np.testing.assert_array_almost_equal(results.x, [0, 0]) self.assertEqual(results.fval, 0.0) def test_qubo_gas_int_simple(self): diff --git a/test/optimization/test_readme_sample.py b/test/optimization/test_readme_sample.py index c4c6137be5..643767de24 100644 --- a/test/optimization/test_readme_sample.py +++ b/test/optimization/test_readme_sample.py @@ -75,7 +75,7 @@ def print(*args): print(result) # prints solution, x=[1, 0, 1, 0], the cost, fval=4 # ---------------------------------------------------------------------- - self.assertListEqual(result.x, [1, 0, 1, 0]) + np.testing.assert_array_almost_equal(result.x, [1, 0, 1, 0]) self.assertAlmostEqual(result.fval, 4.0)