Skip to content

Commit a752fd9

Browse files
t-imamichia-matsuowoodsp-ibmmergify[bot]
authored
Update optimizers to allow new primitives-based algorithms (#436)
* let MinimumEigenOptimizer allow new primitive-based algorithms * update other optimizers * update other tests * add sampler * added unittests * finalized the codes and unittests * update docstring * add reno * update reno, requirements.txt, and tests - Catch deprecation messages in tests * Updates tests to suppress warnings - catch PendingDeprecationWarnings of legacy tests - replace a deprecated networkx function in test_sk_model - replace a deprecated matplotlib function in bin_packing * revert update of matplotlib and networkx * add prelude * Update releasenotes/notes/add-primitives-support-31af39549b5e66e3.yaml Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * update reno * revert grover reno file to avoid sphinx warning * Update releasenotes/notes/add-primitives-support-31af39549b5e66e3.yaml * update sample_most_likely * update reno * Apply Steve's suggestions from code review Co-authored-by: a-matsuo <MATSUOA@jp.ibm.com> Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 3fba471 commit a752fd9

24 files changed

+1192
-243
lines changed

.pylintdict

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ nosignatures
126126
np
127127
num
128128
numpy
129+
numpyminimumeigensolver
129130
october
130131
optimality
131132
optimizationresult

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ from docplex.mp.model import Model
6868
from qiskit_optimization.algorithms import MinimumEigenOptimizer
6969
from qiskit_optimization.translators import from_docplex_mp
7070

71-
from qiskit.utils import algorithm_globals, QuantumInstance
72-
from qiskit import BasicAer
73-
from qiskit.algorithms import QAOA
71+
from qiskit.utils import algorithm_globals
72+
from qiskit.primitives import Sampler
73+
from qiskit.algorithms.minimum_eigensolvers import QAOA
7474
from qiskit.algorithms.optimizers import SPSA
7575

7676
# Generate a graph of 4 nodes
@@ -97,9 +97,8 @@ seed = 1234
9797
algorithm_globals.random_seed = seed
9898

9999
spsa = SPSA(maxiter=250)
100-
backend = BasicAer.get_backend('qasm_simulator')
101-
q_i = QuantumInstance(backend=backend, seed_simulator=seed, seed_transpiler=seed)
102-
qaoa = QAOA(optimizer=spsa, reps=5, quantum_instance=q_i)
100+
sampler = Sampler()
101+
qaoa = QAOA(sampler=sampler, optimizer=spsa, reps=5)
103102
algorithm = MinimumEigenOptimizer(qaoa)
104103
result = algorithm.solve(problem)
105104
print(result.prettyprint()) # prints solution, x=[1, 0, 1, 0], the cost, fval=4

qiskit_optimization/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@
2626
A uniform interface as well as automatic conversion between different problem representations
2727
allows users to solve problems using a large set of algorithms, from variational quantum algorithms,
2828
such as the Quantum Approximate Optimization Algorithm
29-
(:class:`~qiskit.algorithms.QAOA`), to
29+
(:class:`~qiskit.algorithms.minimum_eigensolver.QAOA`), to
3030
`Grover Adaptive Search <https://arxiv.org/abs/quant-ph/9607014>`_
3131
(:class:`~algorithms.GroverOptimizer`), leveraging
32-
fundamental :mod:`~qiskit.algorithms` provided by Qiskit Terra. Furthermore, the modular design
32+
fundamental :mod:`~qiskit.algorithms.minimum_eigensolver` provided by Qiskit Terra.
33+
Furthermore, the modular design
3334
of the optimization module allows it to be easily extended and facilitates rapid development and
3435
testing of new algorithms. Compatible classical optimizers are also provided for testing,
3536
validation, and benchmarking.

qiskit_optimization/algorithms/admm_optimizer.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,21 @@
1717
from typing import List, Optional, Tuple, cast
1818

1919
import numpy as np
20-
from qiskit.algorithms import NumPyMinimumEigensolver
20+
from qiskit.algorithms.minimum_eigensolvers import NumPyMinimumEigensolver
2121

22+
from ..converters import MaximizeToMinimize
23+
from ..problems.constraint import Constraint
24+
from ..problems.linear_constraint import LinearConstraint
25+
from ..problems.linear_expression import LinearExpression
26+
from ..problems.quadratic_program import QuadraticProgram
27+
from ..problems.variable import Variable, VarType
2228
from .minimum_eigen_optimizer import MinimumEigenOptimizer
2329
from .optimization_algorithm import (
24-
OptimizationResultStatus,
2530
OptimizationAlgorithm,
2631
OptimizationResult,
32+
OptimizationResultStatus,
2733
)
2834
from .slsqp_optimizer import SlsqpOptimizer
29-
from ..problems.constraint import Constraint
30-
from ..problems.linear_constraint import LinearConstraint
31-
from ..problems.linear_expression import LinearExpression
32-
from ..problems.quadratic_program import QuadraticProgram
33-
from ..problems.variable import VarType, Variable
34-
from ..converters import MaximizeToMinimize
3535

3636
UPDATE_RHO_BY_TEN_PERCENT = 0
3737
UPDATE_RHO_BY_RESIDUALS = 1

qiskit_optimization/algorithms/grover_optimizer.py

Lines changed: 94 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import math
1717
from copy import deepcopy
1818
from typing import Optional, Dict, Union, List, cast
19+
import warnings
1920

2021
import numpy as np
2122

@@ -24,6 +25,7 @@
2425
from qiskit.utils import QuantumInstance, algorithm_globals
2526
from qiskit.algorithms.amplitude_amplifiers.grover import Grover
2627
from qiskit.circuit.library import QuadraticForm
28+
from qiskit.primitives import BaseSampler
2729
from qiskit.providers import Backend
2830
from qiskit.quantum_info import partial_trace
2931
from .optimization_algorithm import (
@@ -36,6 +38,7 @@
3638
QuadraticProgramToQubo,
3739
QuadraticProgramConverter,
3840
)
41+
from ..exceptions import QiskitOptimizationError
3942
from ..problems import Variable
4043
from ..problems.quadratic_program import QuadraticProgram
4144

@@ -54,6 +57,7 @@ def __init__(
5457
Union[QuadraticProgramConverter, List[QuadraticProgramConverter]]
5558
] = None,
5659
penalty: Optional[float] = None,
60+
sampler: Optional[BaseSampler] = None,
5761
) -> None:
5862
"""
5963
Args:
@@ -66,20 +70,35 @@ def __init__(
6670
:class:`~qiskit_optimization.converters.QuadraticProgramToQubo` will be used.
6771
penalty: The penalty factor used in the default
6872
:class:`~qiskit_optimization.converters.QuadraticProgramToQubo` converter
73+
sampler: A Sampler to use for sampling the results of the circuits.
6974
7075
Raises:
76+
ValueError: If both a quantum instance and sampler are set.
7177
TypeError: When there one of converters is an invalid type.
7278
"""
7379
self._num_value_qubits = num_value_qubits
7480
self._num_key_qubits = 0
7581
self._n_iterations = num_iterations
76-
self._quantum_instance = None # type: Optional[QuantumInstance]
7782
self._circuit_results = {} # type: dict
83+
self._converters = self._prepare_converters(converters, penalty)
7884

79-
if quantum_instance is not None:
80-
self.quantum_instance = quantum_instance
85+
if quantum_instance is not None and sampler is not None:
86+
raise ValueError("Only one of quantum_instance or sampler can be passed, not both!")
8187

82-
self._converters = self._prepare_converters(converters, penalty)
88+
self._quantum_instance = None # type: Optional[QuantumInstance]
89+
if quantum_instance is not None:
90+
warnings.warn(
91+
"The quantum_instance argument has been superseded by the sampler argument. "
92+
"This argument will be deprecated in a future release and subsequently "
93+
"removed after that.",
94+
category=PendingDeprecationWarning,
95+
stacklevel=2,
96+
)
97+
with warnings.catch_warnings():
98+
warnings.simplefilter("ignore", category=PendingDeprecationWarning)
99+
self.quantum_instance = quantum_instance
100+
101+
self._sampler = sampler
83102

84103
@property
85104
def quantum_instance(self) -> QuantumInstance:
@@ -88,6 +107,13 @@ def quantum_instance(self) -> QuantumInstance:
88107
Returns:
89108
The quantum instance used in the algorithm.
90109
"""
110+
warnings.warn(
111+
"The quantum_instance argument has been superseded by the sampler argument. "
112+
"This argument will be deprecated in a future release and subsequently "
113+
"removed after that.",
114+
category=PendingDeprecationWarning,
115+
stacklevel=2,
116+
)
91117
return self._quantum_instance
92118

93119
@quantum_instance.setter
@@ -97,6 +123,13 @@ def quantum_instance(self, quantum_instance: Union[Backend, QuantumInstance]) ->
97123
Args:
98124
quantum_instance: The quantum instance to be used in the algorithm.
99125
"""
126+
warnings.warn(
127+
"The GroverOptimizer.quantum_instance setter is pending deprecation. "
128+
"This property will be deprecated in a future release and subsequently "
129+
"removed after that.",
130+
category=PendingDeprecationWarning,
131+
stacklevel=2,
132+
)
100133
if isinstance(quantum_instance, Backend):
101134
self._quantum_instance = QuantumInstance(quantum_instance)
102135
else:
@@ -162,11 +195,16 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult:
162195
The result of the optimizer applied to the problem.
163196
164197
Raises:
198+
ValueError: If a quantum instance or a sampler has not been provided.
199+
ValueError: If both a quantum instance and sampler are set.
165200
AttributeError: If the quantum instance has not been set.
166201
QiskitOptimizationError: If the problem is incompatible with the optimizer.
167202
"""
168-
if self.quantum_instance is None:
169-
raise AttributeError("The quantum instance or backend has not been set.")
203+
if self._sampler is None and self._quantum_instance is None:
204+
raise ValueError("A quantum instance or sampler must be provided.")
205+
206+
if self._quantum_instance is not None and self._sampler is not None:
207+
raise ValueError("Only one of quantum_instance or sampler can be passed, not both!")
170208

171209
self._verify_compatibility(problem)
172210

@@ -199,7 +237,7 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult:
199237
# Initialize oracle helper object.
200238
qr_key_value = QuantumRegister(self._num_key_qubits + self._num_value_qubits)
201239
orig_constant = problem_.objective.constant
202-
measurement = not self.quantum_instance.is_statevector
240+
measurement = self._quantum_instance is None or not self._quantum_instance.is_statevector
203241
oracle, is_good_state = self._get_oracle(qr_key_value)
204242

205243
while not optimum_found:
@@ -246,15 +284,19 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult:
246284
threshold = optimum_value
247285

248286
# trace out work qubits and store samples
249-
if self._quantum_instance.is_statevector:
250-
indices = list(range(n_key, len(outcome)))
251-
rho = partial_trace(self._circuit_results, indices)
252-
self._circuit_results = cast(Dict, np.diag(rho.data) ** 0.5)
253-
else:
287+
if self._sampler is not None:
254288
self._circuit_results = {
255289
i[-1 * n_key :]: v for i, v in self._circuit_results.items()
256290
}
257-
291+
else:
292+
if self._quantum_instance.is_statevector:
293+
indices = list(range(n_key, len(outcome)))
294+
rho = partial_trace(self._circuit_results, indices)
295+
self._circuit_results = cast(Dict, np.diag(rho.data) ** 0.5)
296+
else:
297+
self._circuit_results = {
298+
i[-1 * n_key :]: v for i, v in self._circuit_results.items()
299+
}
258300
raw_samples = self._eigenvector_to_solutions(
259301
self._circuit_results, problem_init
260302
)
@@ -312,33 +354,52 @@ def solve(self, problem: QuadraticProgram) -> OptimizationResult:
312354

313355
def _measure(self, circuit: QuantumCircuit) -> str:
314356
"""Get probabilities from the given backend, and picks a random outcome."""
315-
probs = self._get_probs(circuit)
357+
probs = self._get_prob_dist(circuit)
316358
logger.info("Frequencies: %s", probs)
317359
# Pick a random outcome.
318360
return algorithm_globals.random.choice(list(probs.keys()), 1, p=list(probs.values()))[0]
319361

320-
def _get_probs(self, qc: QuantumCircuit) -> Dict[str, float]:
362+
def _get_prob_dist(self, qc: QuantumCircuit) -> Dict[str, float]:
321363
"""Gets probabilities from a given backend."""
322364
# Execute job and filter results.
323-
result = self.quantum_instance.execute(qc)
324-
if self.quantum_instance.is_statevector:
325-
state = result.get_statevector(qc)
326-
if not isinstance(state, np.ndarray):
327-
state = state.data
328-
keys = [
329-
bin(i)[2::].rjust(int(np.log2(len(state))), "0")[::-1] for i in range(0, len(state))
330-
]
331-
probs = [abs(a) ** 2 for a in state]
332-
total = math.fsum(probs)
333-
probs = [p / total for p in probs]
334-
hist = {key: prob for key, prob in zip(keys, probs) if prob > 0}
335-
self._circuit_results = state
365+
if self._sampler is not None:
366+
job = self._sampler.run([qc])
367+
368+
try:
369+
result = job.result()
370+
except Exception as exc:
371+
raise QiskitOptimizationError("Sampler job failed.") from exc
372+
quasi_dist = result.quasi_dists[0]
373+
bit_length = (len(quasi_dist) - 1).bit_length()
374+
prob_dist = {f"{i:0{bit_length}b}"[::-1]: v for i, v in quasi_dist.items()}
375+
self._circuit_results = {
376+
f"{i:0{bit_length}b}": v**0.5
377+
for i, v in quasi_dist.items()
378+
if not np.isclose(v, 0)
379+
}
336380
else:
337-
state = result.get_counts(qc)
338-
shots = self.quantum_instance.run_config.shots
339-
hist = {key[::-1]: val / shots for key, val in sorted(state.items()) if val > 0}
340-
self._circuit_results = {b: (v / shots) ** 0.5 for (b, v) in state.items()}
341-
return hist
381+
result = self._quantum_instance.execute(qc)
382+
if self._quantum_instance.is_statevector:
383+
state = result.get_statevector(qc)
384+
if not isinstance(state, np.ndarray):
385+
state = state.data
386+
keys = [
387+
bin(i)[2::].rjust(int(np.log2(len(state))), "0")[::-1]
388+
for i in range(0, len(state))
389+
]
390+
probs = [abs(a) ** 2 for a in state]
391+
total = math.fsum(probs)
392+
probs = [p / total for p in probs]
393+
prob_dist = {key: prob for key, prob in zip(keys, probs) if prob > 0}
394+
self._circuit_results = state
395+
else:
396+
state = result.get_counts(qc)
397+
shots = self._quantum_instance.run_config.shots
398+
prob_dist = {
399+
key[::-1]: val / shots for key, val in sorted(state.items()) if val > 0
400+
}
401+
self._circuit_results = {b: (v / shots) ** 0.5 for (b, v) in state.items()}
402+
return prob_dist
342403

343404
@staticmethod
344405
def _bin_to_int(v: str, num_value_bits: int) -> int:

qiskit_optimization/algorithms/minimum_eigen_optimizer.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This code is part of Qiskit.
22
#
3-
# (C) Copyright IBM 2020, 2021.
3+
# (C) Copyright IBM 2020, 2022.
44
#
55
# This code is licensed under the Apache License, Version 2.0. You may
66
# obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -11,24 +11,37 @@
1111
# that they have been altered from the originals.
1212

1313
"""A wrapper for minimum eigen solvers to be used within the optimization module."""
14-
from typing import Optional, Union, List, cast
14+
from typing import List, Optional, Union, cast
1515

1616
import numpy as np
17+
from qiskit.algorithms.minimum_eigen_solvers import MinimumEigensolver as LegacyMinimumEigensolver
18+
from qiskit.algorithms.minimum_eigen_solvers import (
19+
MinimumEigensolverResult as LegacyMinimumEigensolverResult,
20+
)
21+
from qiskit.algorithms.minimum_eigensolvers import (
22+
NumPyMinimumEigensolver,
23+
NumPyMinimumEigensolverResult,
24+
SamplingMinimumEigensolver,
25+
SamplingMinimumEigensolverResult,
26+
)
27+
from qiskit.opflow import OperatorBase, PauliOp, PauliSumOp
1728

18-
from qiskit.algorithms import MinimumEigensolver, MinimumEigensolverResult
19-
from qiskit.opflow import OperatorBase
29+
from ..converters.quadratic_program_to_qubo import QuadraticProgramConverter, QuadraticProgramToQubo
30+
from ..exceptions import QiskitOptimizationError
31+
from ..problems.quadratic_program import QuadraticProgram, Variable
2032
from .optimization_algorithm import (
21-
OptimizationResultStatus,
2233
OptimizationAlgorithm,
2334
OptimizationResult,
35+
OptimizationResultStatus,
2436
SolutionSample,
2537
)
26-
from ..exceptions import QiskitOptimizationError
27-
from ..converters.quadratic_program_to_qubo import (
28-
QuadraticProgramToQubo,
29-
QuadraticProgramConverter,
30-
)
31-
from ..problems.quadratic_program import QuadraticProgram, Variable
38+
39+
MinimumEigensolver = Union[
40+
SamplingMinimumEigensolver, NumPyMinimumEigensolver, LegacyMinimumEigensolver
41+
]
42+
MinimumEigensolverResult = Union[
43+
SamplingMinimumEigensolverResult, NumPyMinimumEigensolverResult, LegacyMinimumEigensolverResult
44+
]
3245

3346

3447
class MinimumEigenOptimizationResult(OptimizationResult):
@@ -101,7 +114,7 @@ class MinimumEigenOptimizer(OptimizationAlgorithm):
101114
102115
.. code-block::
103116
104-
from qiskit.algorithms import QAOA
117+
from qiskit.algorithms.minimum_eigensolver import QAOA
105118
from qiskit_optimization.problems import QuadraticProgram
106119
from qiskit_optimization.algorithms import MinimumEigenOptimizer
107120
problem = QuadraticProgram()
@@ -141,7 +154,7 @@ def __init__(
141154
if not min_eigen_solver.supports_aux_operators():
142155
raise QiskitOptimizationError(
143156
"Given MinimumEigensolver does not return the eigenstate "
144-
+ "and is not supported by the MinimumEigenOptimizer."
157+
"and is not supported by the MinimumEigenOptimizer."
145158
)
146159
self._min_eigen_solver = min_eigen_solver
147160
self._penalty = penalty
@@ -206,6 +219,9 @@ def _solve_internal(
206219
# only try to solve non-empty Ising Hamiltonians
207220
eigen_result: Optional[MinimumEigensolverResult] = None
208221
if operator.num_qubits > 0:
222+
# NumPyEigensolver does not accept PauliOp but PauliSumOp
223+
if isinstance(operator, PauliOp):
224+
operator = PauliSumOp.from_list([(operator.primitive.to_label(), operator.coeff)])
209225
# approximate ground state of operator using min eigen solver
210226
eigen_result = self._min_eigen_solver.compute_minimum_eigenvalue(operator)
211227
# analyze results

0 commit comments

Comments
 (0)