Skip to content

Commit 7a8fa47

Browse files
KahanMajmudarmanoelmarquest-imamichiadekusar-drlwoodsp-ibm
authored andcommitted
Add feasibility checks to optimizers/converters (qiskit-community#1199)
* Draft for issue#1134 * resolved issue in quadratic_problem.py * incorporated changes as suggested by @adekusar-drl * fix linting issues * added is_feasible method in all optimizers, added changes suggested by @ woodsp-ibm * removed cast import * made changes as per points 1 to 3 * made suggested changes by @adekusar-drl * some cleaning * added reno * fixed reno * Update releasenotes/notes/feasibility-check-b99605f771e745b7.yaml Co-authored-by: Steve Wood <[email protected]> * Update releasenotes/notes/feasibility-check-b99605f771e745b7.yaml Co-authored-by: Steve Wood <[email protected]> * code review Co-authored-by: Manoel Marques <[email protected]> Co-authored-by: Takashi Imamichi <[email protected]> Co-authored-by: Anton Dekusar <[email protected]> Co-authored-by: Anton Dekusar <[email protected]> Co-authored-by: Steve Wood <[email protected]>
1 parent 427df71 commit 7a8fa47

15 files changed

+207
-60
lines changed

qiskit/optimization/algorithms/admm_optimizer.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
from qiskit.aqua.algorithms import NumPyMinimumEigensolver
2222

2323
from .minimum_eigen_optimizer import MinimumEigenOptimizer
24-
from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult
24+
from .optimization_algorithm import (OptimizationResultStatus, OptimizationAlgorithm,
25+
OptimizationResult)
2526
from .slsqp_optimizer import SlsqpOptimizer
2627
from ..problems.constraint import Constraint
2728
from ..problems.linear_constraint import LinearConstraint
@@ -180,15 +181,16 @@ class ADMMOptimizationResult(OptimizationResult):
180181
""" ADMMOptimization Result."""
181182

182183
def __init__(self, x: np.ndarray, fval: float, variables: List[Variable],
183-
state: ADMMState) -> None:
184+
state: ADMMState, status: OptimizationResultStatus) -> None:
184185
"""
185186
Args:
186187
x: the optimal value found by ADMM.
187188
fval: the optimal function value.
188189
variables: the list of variables of the optimization problem.
189190
state: the internal computation state of ADMM.
191+
status: Termination status of an optimization algorithm
190192
"""
191-
super().__init__(x=x, fval=fval, variables=variables, raw_results=state)
193+
super().__init__(x=x, fval=fval, variables=variables, status=status, raw_results=state)
192194

193195
@property
194196
def state(self) -> ADMMState:
@@ -375,13 +377,15 @@ def solve(self, problem: QuadraticProgram) -> ADMMOptimizationResult:
375377
objective_value = objective_value * sense
376378

377379
# convert back integer to binary
378-
base_result = OptimizationResult(solution, objective_value, original_variables)
380+
base_result = OptimizationResult(solution, objective_value, original_variables,
381+
OptimizationResultStatus.SUCCESS)
379382
base_result = int2bin.interpret(base_result)
380383

381384
# third parameter is our internal state of computations.
382385
result = ADMMOptimizationResult(x=base_result.x, fval=base_result.fval,
383386
variables=base_result.variables,
384-
state=self._state)
387+
state=self._state,
388+
status=self._get_feasibility_status(problem, base_result.x))
385389

386390
# debug
387391
self._log.debug("solution=%s, objective=%s at iteration=%s",

qiskit/optimization/algorithms/cplex_optimizer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult:
136136
sol = cplex.solution
137137

138138
# create results
139-
result = OptimizationResult(x=sol.get_values(),
140-
fval=sol.get_objective_value(),
139+
result = OptimizationResult(x=sol.get_values(), fval=sol.get_objective_value(),
141140
variables=problem.variables,
141+
status=self._get_feasibility_status(problem, sol.get_values()),
142142
raw_results=sol)
143143

144144
# return solution

qiskit/optimization/algorithms/grover_optimizer.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
from qiskit.aqua.algorithms.amplitude_amplifiers.grover import Grover
2525
from qiskit.aqua.components.initial_states import Custom
2626
from qiskit.aqua.components.oracles import CustomCircuitOracle
27-
from qiskit.circuit.library import QuadraticForm
2827
from qiskit.providers import BaseBackend
29-
from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult
28+
from qiskit.circuit.library import QuadraticForm
29+
from .optimization_algorithm import (OptimizationResultStatus, OptimizationAlgorithm,
30+
OptimizationResult)
3031
from ..converters.quadratic_program_to_qubo import QuadraticProgramToQubo
3132
from ..problems import Variable
3233
from ..problems.quadratic_program import QuadraticProgram
@@ -261,15 +262,17 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult:
261262

262263
# Compute function value
263264
fval = problem_init.objective.evaluate(opt_x)
264-
result = OptimizationResult(x=opt_x, fval=fval, variables=problem_.variables)
265+
result = OptimizationResult(x=opt_x, fval=fval, variables=problem_.variables,
266+
status=OptimizationResultStatus.SUCCESS)
265267

266268
# cast binaries back to integers
267269
result = self._qubo_converter.interpret(result)
268270

269271
return GroverOptimizationResult(x=result.x, fval=result.fval, variables=result.variables,
270272
operation_counts=operation_count, n_input_qubits=n_key,
271273
n_output_qubits=n_value, intermediate_fval=fval,
272-
threshold=threshold)
274+
threshold=threshold,
275+
status=self._get_feasibility_status(problem, result.x))
273276

274277
def _measure(self, circuit: QuantumCircuit) -> str:
275278
"""Get probabilities from the given backend, and picks a random outcome."""
@@ -333,7 +336,8 @@ class GroverOptimizationResult(OptimizationResult):
333336

334337
def __init__(self, x: Union[List[float], np.ndarray], fval: float, variables: List[Variable],
335338
operation_counts: Dict[int, Dict[str, int]], n_input_qubits: int,
336-
n_output_qubits: int, intermediate_fval: float, threshold: float) -> None:
339+
n_output_qubits: int, intermediate_fval: float, threshold: float,
340+
status: OptimizationResultStatus) -> None:
337341
"""
338342
Constructs a result object with the specific Grover properties.
339343
@@ -347,8 +351,9 @@ def __init__(self, x: Union[List[float], np.ndarray], fval: float, variables: Li
347351
intermediate_fval: The intermediate value of the objective function of the solution,
348352
that is expected to be identical with ``fval``.
349353
threshold: The threshold of Grover algorithm.
354+
status: the termination status of the optimization algorithm.
350355
"""
351-
super().__init__(x, fval, variables, None)
356+
super().__init__(x, fval, variables, status, None)
352357
self._operation_counts = operation_counts
353358
self._n_input_qubits = n_input_qubits
354359
self._n_output_qubits = n_output_qubits

qiskit/optimization/algorithms/minimum_eigen_optimizer.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,28 @@
1717
from qiskit.aqua.algorithms import MinimumEigensolver, MinimumEigensolverResult
1818
from qiskit.aqua.operators import StateFn, DictStateFn
1919

20-
from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult
20+
from .optimization_algorithm import (OptimizationResultStatus, OptimizationAlgorithm,
21+
OptimizationResult)
2122
from ..converters.quadratic_program_to_qubo import QuadraticProgramToQubo
2223
from ..problems.quadratic_program import QuadraticProgram, Variable
2324

2425

2526
class MinimumEigenOptimizationResult(OptimizationResult):
2627
""" Minimum Eigen Optimizer Result."""
2728

28-
def __init__(self, x: Union[List[float], np.ndarray], fval: float,
29-
variables: List[Variable],
30-
samples: List[Tuple[str, float, float]],
29+
def __init__(self, x: Union[List[float], np.ndarray], fval: float, variables: List[Variable],
30+
status: OptimizationResultStatus, samples: List[Tuple[str, float, float]],
3131
min_eigen_solver_result: Optional[MinimumEigensolverResult] = None) -> None:
3232
"""
3333
Args:
3434
x: the optimal value found by ``MinimumEigensolver``.
3535
fval: the optimal function value.
3636
variables: the list of variables of the optimization problem.
37+
status: the termination status of the optimization algorithm.
3738
samples: the basis state as bitstring, the QUBO value, and the probability of sampling.
3839
min_eigen_solver_result: the result obtained from the underlying algorithm.
3940
"""
40-
super().__init__(x, fval, variables, None)
41+
super().__init__(x, fval, variables, status, None)
4142
self._samples = samples
4243
self._min_eigen_solver_result = min_eigen_solver_result
4344

@@ -178,12 +179,15 @@ def solve(self, problem: QuadraticProgram) -> MinimumEigenOptimizationResult:
178179
samples = [(x_str, offset, 1.0)]
179180

180181
# translate result back to integers
181-
result = OptimizationResult(x=x, fval=fval, variables=problem_.variables)
182+
result = OptimizationResult(x=x, fval=fval, variables=problem_.variables,
183+
status=OptimizationResultStatus.SUCCESS)
182184
result = self._qubo_converter.interpret(result)
185+
183186
return MinimumEigenOptimizationResult(x=result.x, fval=result.fval,
184187
variables=result.variables,
185-
samples=samples,
186-
min_eigen_solver_result=eigen_result)
188+
status=self._get_feasibility_status(problem,
189+
result.x),
190+
samples=samples, min_eigen_solver_result=eigen_result)
187191

188192

189193
def _eigenvector_to_solutions(eigenvector: Union[dict, np.ndarray, StateFn],

qiskit/optimization/algorithms/multistart_optimizer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
from scipy.stats import uniform
2424

2525
from qiskit.optimization import QuadraticProgram, INFINITY
26-
from qiskit.optimization.algorithms.optimization_algorithm import (OptimizationAlgorithm,
27-
OptimizationResult)
26+
from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult
2827

2928
logger = logging.getLogger(__name__)
3029

@@ -93,6 +92,7 @@ def multi_start_solve(self, minimize: Callable[[np.array], Tuple[np.array, Any]]
9392
rest_sol = rest
9493

9594
return OptimizationResult(x=x_sol, fval=fval_sol, variables=problem.variables,
95+
status=self._get_feasibility_status(problem, x_sol),
9696
raw_results=rest_sol)
9797

9898
@property

qiskit/optimization/algorithms/optimization_algorithm.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@
2222
from ..problems.quadratic_program import QuadraticProgram, Variable
2323

2424

25+
class OptimizationResultStatus(Enum):
26+
"""Termination status of an optimization algorithm."""
27+
28+
SUCCESS = 0
29+
"""the optimization algorithm succeeded to find an optimal solution."""
30+
31+
FAILURE = 1
32+
"""the optimization algorithm ended in a failure."""
33+
34+
INFEASIBLE = 2
35+
"""the optimization algorithm obtained an infeasible solution."""
36+
37+
2538
class OptimizationAlgorithm(ABC):
2639
"""An abstract class for optimization algorithms in Qiskit's optimization module."""
2740

@@ -84,18 +97,21 @@ def _verify_compatibility(self, problem: QuadraticProgram) -> None:
8497
if msg:
8598
raise QiskitOptimizationError('Incompatible problem: {}'.format(msg))
8699

100+
def _get_feasibility_status(self, problem: QuadraticProgram,
101+
x: Union[List[float], np.ndarray]) -> OptimizationResultStatus:
102+
"""Returns whether the input result is feasible or not for the given problem.
87103
88-
class OptimizationResultStatus(Enum):
89-
"""Termination status of an optimization algorithm."""
90-
91-
SUCCESS = 0
92-
"""the optimization algorithm succeeded to find an optimal solution."""
104+
Args:
105+
problem: Problem to verify.
106+
x: the input result list.
93107
94-
FAILURE = 1
95-
"""the optimization algorithm ended in a failure."""
108+
Returns:
109+
The status of the result.
110+
"""
111+
is_feasible = problem.is_feasible(x)
96112

97-
INFEASIBLE = 2
98-
"""the optimization algorithm obtained an infeasible solution."""
113+
return OptimizationResultStatus.SUCCESS if is_feasible \
114+
else OptimizationResultStatus.INFEASIBLE
99115

100116

101117
class OptimizationResult:
@@ -140,8 +156,8 @@ class OptimizationResult:
140156

141157
def __init__(self, x: Union[List[float], np.ndarray], fval: float,
142158
variables: List[Variable],
143-
raw_results: Optional[Any] = None,
144-
status: OptimizationResultStatus = OptimizationResultStatus.SUCCESS) -> None:
159+
status: OptimizationResultStatus,
160+
raw_results: Optional[Any] = None) -> None:
145161
"""
146162
Args:
147163
x: the optimal value found in the optimization.

qiskit/optimization/algorithms/recursive_minimum_eigen_optimizer.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
from qiskit.aqua.algorithms import NumPyMinimumEigensolver
2222
from qiskit.aqua.utils.validation import validate_min
2323

24-
from .optimization_algorithm import OptimizationAlgorithm, OptimizationResult
24+
from .optimization_algorithm import (OptimizationResultStatus, OptimizationAlgorithm,
25+
OptimizationResult)
2526
from .minimum_eigen_optimizer import MinimumEigenOptimizer, MinimumEigenOptimizationResult
2627
from ..converters.quadratic_program_to_qubo import QuadraticProgramToQubo
2728
from ..exceptions import QiskitOptimizationError
@@ -50,9 +51,8 @@ class IntermediateResult(Enum):
5051

5152
class RecursiveMinimumEigenOptimizationResult(OptimizationResult):
5253
"""Recursive Eigen Optimizer Result."""
53-
def __init__(self, x: Union[List[float], np.ndarray], fval: float,
54-
variables: List[Variable],
55-
replacements: Dict[str, Tuple[str, int]],
54+
def __init__(self, x: Union[List[float], np.ndarray], fval: float, variables: List[Variable],
55+
status: OptimizationResultStatus, replacements: Dict[str, Tuple[str, int]],
5656
history: Tuple[List[MinimumEigenOptimizationResult], OptimizationResult]) -> None:
5757
"""
5858
Constructs an instance of the result class.
@@ -61,6 +61,7 @@ def __init__(self, x: Union[List[float], np.ndarray], fval: float,
6161
x: the optimal value found in the optimization.
6262
fval: the optimal function value.
6363
variables: the list of variables of the optimization problem.
64+
status: the termination status of the optimization algorithm.
6465
replacements: a dictionary of substituted variables. Key is a variable being
6566
substituted, value is a tuple of substituting variable and a weight, either 1 or -1.
6667
history: a tuple containing intermediate results. The first element is a list of
@@ -70,7 +71,7 @@ def __init__(self, x: Union[List[float], np.ndarray], fval: float,
7071
:class:`~qiskit.optimization.algorithm.OptimizationResult` obtained at the last step
7172
via `min_num_vars_optimizer`.
7273
"""
73-
super().__init__(x, fval, variables, None)
74+
super().__init__(x, fval, variables, status, None)
7475
self._replacements = replacements
7576
self._history = history
7677

@@ -281,13 +282,16 @@ def find_value(x, replacements, var_values):
281282
# construct result
282283
x_v = [var_values[x_aux.name] for x_aux in problem_ref.variables]
283284
fval = result.fval
284-
result = OptimizationResult(x=x_v, fval=fval, variables=problem_ref.variables)
285+
result = OptimizationResult(x=x_v, fval=fval, variables=problem_ref.variables,
286+
status=OptimizationResultStatus.SUCCESS)
285287
result = self._qubo_converter.interpret(result)
286288

287289
return RecursiveMinimumEigenOptimizationResult(x=result.x, fval=result.fval,
288290
variables=result.variables,
289291
replacements=replacements,
290-
history=history)
292+
history=history,
293+
status=(self._get_feasibility_status
294+
(problem, result.x)))
291295

292296
def _find_strongest_correlation(self, correlations):
293297

qiskit/optimization/algorithms/slsqp_optimizer.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from scipy.optimize import fmin_slsqp
1919

2020
from .multistart_optimizer import MultiStartOptimizer
21-
from .optimization_algorithm import OptimizationResult
21+
from .optimization_algorithm import OptimizationResultStatus, OptimizationResult
2222
from ..exceptions import QiskitOptimizationError
2323
from ..problems import Variable
2424
from ..problems.constraint import Constraint
@@ -32,8 +32,9 @@ class SlsqpOptimizationResult(OptimizationResult):
3232
SLSQP optimization result, defines additional properties that may be returned by the optimizer.
3333
"""
3434
def __init__(self, x: Union[List[float], np.ndarray], fval: float, variables: List[Variable],
35-
fx: Optional[np.ndarray] = None, its: Optional[int] = None,
36-
imode: Optional[int] = None, smode: Optional[str] = None) -> None:
35+
status: OptimizationResultStatus, fx: Optional[np.ndarray] = None,
36+
its: Optional[int] = None, imode: Optional[int] = None,
37+
smode: Optional[str] = None) -> None:
3738
"""
3839
Constructs a result object with properties specific to SLSQP.
3940
@@ -46,8 +47,9 @@ def __init__(self, x: Union[List[float], np.ndarray], fval: float, variables: Li
4647
imode: The exit mode from the optimizer
4748
(see the documentation of ``scipy.optimize.fmin_slsqp``).
4849
smode: Message describing the exit mode from the optimizer.
50+
status: the termination status of the optimization algorithm.
4951
"""
50-
super().__init__(x, fval, variables, None)
52+
super().__init__(x, fval, variables, status, None)
5153
self._fx = fx
5254
self._its = its
5355
self._imode = imode
@@ -217,8 +219,10 @@ def _minimize(x_0: np.array) -> Tuple[np.array, Any]:
217219
result = self.multi_start_solve(_minimize, problem)
218220

219221
if self._full_output:
220-
return SlsqpOptimizationResult(result.x, result.fval, result.variables,
222+
return SlsqpOptimizationResult(x=result.x, fval=result.fval, variables=result.variables,
223+
status=self._get_feasibility_status(problem, result.x),
221224
fx=result.raw_results[0], its=result.raw_results[1],
222225
imode=result.raw_results[2], smode=result.raw_results[3])
223226
else:
224-
return SlsqpOptimizationResult(result.x, result.fval, result.variables)
227+
return SlsqpOptimizationResult(x=result.x, fval=result.fval, variables=result.variables,
228+
status=self._get_feasibility_status(problem, result.x))

qiskit/optimization/converters/inequality_to_equality.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def interpret(self, result: OptimizationResult) -> OptimizationResult:
352352
names = [x.name for x in self._dst.variables]
353353
new_x = self._interpret_var(names, result.x)
354354
return OptimizationResult(x=new_x, fval=result.fval, variables=self._src.variables,
355-
raw_results=result.raw_results, status=result.status)
355+
status=result.status, raw_results=result.raw_results)
356356

357357
def _interpret_var(self, names, vals) -> List[int]:
358358
# interpret slack variables

qiskit/optimization/converters/integer_to_binary.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def interpret(self, result: OptimizationResult) -> OptimizationResult:
219219
"""
220220
new_x = self._interpret_var(result.x)
221221
return OptimizationResult(x=new_x, fval=result.fval, variables=self._src.variables,
222-
raw_results=result.raw_results)
222+
status=result.status, raw_results=result.raw_results)
223223

224224
def _interpret_var(self, vals: Union[List[float], np.ndarray]) -> List[float]:
225225
# interpret integer values

qiskit/optimization/converters/linear_equality_to_penalty.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,8 @@ def interpret(self, result: OptimizationResult) -> OptimizationResult:
190190
new_status = OptimizationResultStatus.INFEASIBLE
191191

192192
return OptimizationResult(x=result.x, fval=substituted_qp.objective.constant,
193-
variables=self._src.variables, raw_results=result.raw_results,
194-
status=new_status)
193+
variables=self._src.variables, status=new_status,
194+
raw_results=result.raw_results)
195195

196196
@property
197197
def penalty(self) -> Optional[float]:

0 commit comments

Comments
 (0)