Skip to content

Commit 51fd88e

Browse files
CryorisElePTmergify[bot]woodsp-ibm
authored
Switch to primitive support in QNSPSA (Qiskit/qiskit#8682)
* switch to primitive support in QNSPSA * attempt to fix sphinx & cyclic import * add comment about sampler passed positionally * Update typehints * revert import * Apply suggestions from code review Co-authored-by: ElePT <epenatap@gmail.com> Co-authored-by: ElePT <57907331+ElePT@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com>
1 parent ee5c98f commit 51fd88e

File tree

2 files changed

+149
-18
lines changed

2 files changed

+149
-18
lines changed

qiskit_algorithms/optimizers/qnspsa.py

Lines changed: 121 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,19 @@
1212

1313
"""The QN-SPSA optimizer."""
1414

15-
from typing import Any, Iterator, Optional, Union, Callable, Dict
15+
from __future__ import annotations
16+
from typing import Any, Iterator, Callable
17+
import warnings
1618

1719
import numpy as np
1820
from qiskit.providers import Backend
1921
from qiskit.circuit import ParameterVector, QuantumCircuit
2022
from qiskit.opflow import StateFn, CircuitSampler, ExpectationBase
2123
from qiskit.utils import QuantumInstance
2224

25+
from qiskit.primitives import BaseSampler, Sampler
26+
from qiskit.algorithms.state_fidelities import ComputeUncompute
27+
2328
from .spsa import SPSA, CALLBACK, TERMINATIONCHECKER, _batch_evaluate
2429

2530
# the function to compute the fidelity
@@ -55,6 +60,37 @@ class QNSPSA(SPSA):
5560
This short example runs QN-SPSA for the ground state calculation of the ``Z ^ Z``
5661
observable where the ansatz is a ``PauliTwoDesign`` circuit.
5762
63+
.. code-block:: python
64+
65+
import numpy as np
66+
from qiskit.algorithms.optimizers import QNSPSA
67+
from qiskit.circuit.library import PauliTwoDesign
68+
from qiskit.primitives import Estimator, Sampler
69+
from qiskit.quantum_info import Pauli
70+
71+
# problem setup
72+
ansatz = PauliTwoDesign(2, reps=1, seed=2)
73+
observable = Pauli("ZZ")
74+
initial_point = np.random.random(ansatz.num_parameters)
75+
76+
# loss function
77+
estimator = Estimator()
78+
79+
def loss(x):
80+
result = estimator.run([ansatz], [observable], [x]).result()
81+
return np.real(result.values[0])
82+
83+
# fidelity for estimation of the geometric tensor
84+
sampler = Sampler()
85+
fidelity = QNSPSA.get_fidelity(ansatz, sampler)
86+
87+
# run QN-SPSA
88+
qnspsa = QNSPSA(fidelity, maxiter=300)
89+
result = qnspsa.optimize(ansatz.num_parameters, loss, initial_point=initial_point)
90+
91+
This is a legacy version solving the same problem but using Qiskit Opflow instead
92+
of the Qiskit Primitives. Note however, that this usage is pending deprecation.
93+
5894
.. code-block:: python
5995
6096
import numpy as np
@@ -87,18 +123,17 @@ def __init__(
87123
fidelity: FIDELITY,
88124
maxiter: int = 100,
89125
blocking: bool = True,
90-
allowed_increase: Optional[float] = None,
91-
learning_rate: Optional[Union[float, Callable[[], Iterator]]] = None,
92-
perturbation: Optional[Union[float, Callable[[], Iterator]]] = None,
93-
last_avg: int = 1,
94-
resamplings: Union[int, Dict[int, int]] = 1,
95-
perturbation_dims: Optional[int] = None,
96-
regularization: Optional[float] = None,
126+
allowed_increase: float | None = None,
127+
learning_rate: float | Callable[[], Iterator] | None = None,
128+
perturbation: float | Callable[[], Iterator] | None = None,
129+
resamplings: int | dict[int, int] = 1,
130+
perturbation_dims: int | None = None,
131+
regularization: float | None = None,
97132
hessian_delay: int = 0,
98-
lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None,
99-
initial_hessian: Optional[np.ndarray] = None,
100-
callback: Optional[CALLBACK] = None,
101-
termination_checker: Optional[TERMINATIONCHECKER] = None,
133+
lse_solver: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = None,
134+
initial_hessian: np.ndarray | None = None,
135+
callback: CALLBACK | None = None,
136+
termination_checker: TERMINATIONCHECKER | None = None,
102137
) -> None:
103138
r"""
104139
Args:
@@ -121,8 +156,6 @@ def __init__(
121156
approximation of the gradients. Can be either a float or a generator yielding
122157
the perturbation magnitudes per step.
123158
If ``perturbation`` is set ``learning_rate`` must also be provided.
124-
last_avg: Return the average of the ``last_avg`` parameters instead of just the
125-
last parameter values.
126159
resamplings: The number of times the gradient (and Hessian) is sampled using a random
127160
direction to construct a gradient estimate. Per default the gradient is estimated
128161
using only one random direction. If an integer, all iterations use the same number
@@ -206,7 +239,7 @@ def _point_sample(self, loss, x, eps, delta1, delta2):
206239
return np.mean(loss_values), gradient_estimate, hessian_estimate
207240

208241
@property
209-
def settings(self) -> Dict[str, Any]:
242+
def settings(self) -> dict[str, Any]:
210243
"""The optimizer settings in a dictionary format."""
211244
# re-use serialization from SPSA
212245
settings = super().settings
@@ -221,11 +254,82 @@ def settings(self) -> Dict[str, Any]:
221254
@staticmethod
222255
def get_fidelity(
223256
circuit: QuantumCircuit,
224-
backend: Optional[Union[Backend, QuantumInstance]] = None,
225-
expectation: Optional[ExpectationBase] = None,
257+
backend: Backend | QuantumInstance | None = None,
258+
expectation: ExpectationBase | None = None,
259+
*,
260+
sampler: BaseSampler | None = None,
226261
) -> Callable[[np.ndarray, np.ndarray], float]:
227262
r"""Get a function to compute the fidelity of ``circuit`` with itself.
228263
264+
.. note::
265+
266+
Using this function with a backend and expectation converter is pending deprecation,
267+
instead pass a Qiskit Primitive sampler, such as :class:`~.Sampler`.
268+
The sampler can be passed as keyword argument or, positionally, as second argument.
269+
270+
Let ``circuit`` be a parameterized quantum circuit performing the operation
271+
:math:`U(\theta)` given a set of parameters :math:`\theta`. Then this method returns
272+
a function to evaluate
273+
274+
.. math::
275+
276+
F(\theta, \phi) = \big|\langle 0 | U^\dagger(\theta) U(\phi) |0\rangle \big|^2.
277+
278+
The output of this function can be used as input for the ``fidelity`` to the
279+
:class:~`qiskit.algorithms.optimizers.QNSPSA` optimizer.
280+
281+
Args:
282+
circuit: The circuit preparing the parameterized ansatz.
283+
backend: *Pending deprecation.* A backend of quantum instance to evaluate the circuits.
284+
If None, plain matrix multiplication will be used.
285+
expectation: *Pending deprecation.* An expectation converter to specify how the expected
286+
value is computed. If a shot-based readout is used this should be set to
287+
``PauliExpectation``.
288+
sampler: A sampler primitive to sample from a quantum state.
289+
290+
Returns:
291+
A handle to the function :math:`F`.
292+
293+
"""
294+
# allow passing sampler by position
295+
if isinstance(backend, BaseSampler):
296+
sampler = backend
297+
backend = None
298+
299+
if expectation is None and backend is None and sampler is None:
300+
sampler = Sampler()
301+
302+
if expectation is not None or backend is not None:
303+
warnings.warn(
304+
"Passing a backend and expectation converter to QNSPSA.get_fidelity is pending "
305+
"deprecation and will be deprecated in a future release. Instead, pass a "
306+
"sampler primitive.",
307+
stacklevel=2,
308+
category=PendingDeprecationWarning,
309+
)
310+
return QNSPSA._legacy_get_fidelity(circuit, backend, expectation)
311+
312+
fid = ComputeUncompute(sampler)
313+
314+
def fidelity(values_x, values_y):
315+
result = fid.run(circuit, circuit, values_x, values_y).result()
316+
return np.asarray(result.fidelities)
317+
318+
return fidelity
319+
320+
@staticmethod
321+
def _legacy_get_fidelity(
322+
circuit: QuantumCircuit,
323+
backend: Backend | QuantumInstance | None = None,
324+
expectation: ExpectationBase | None = None,
325+
) -> Callable[[np.ndarray, np.ndarray], float]:
326+
r"""PENDING DEPRECATION. Get a function to compute the fidelity of ``circuit`` with itself.
327+
328+
.. note::
329+
330+
This method is pending deprecation. Instead use the :class:`~.ComputeUncompute`
331+
class which implements the fidelity calculation in the same fashion as this method.
332+
229333
Let ``circuit`` be a parameterized quantum circuit performing the operation
230334
:math:`U(\theta)` given a set of parameters :math:`\theta`. Then this method returns
231335
a function to evaluate

test/optimizers/test_spsa.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919

2020
from qiskit.algorithms.optimizers import SPSA, QNSPSA
2121
from qiskit.circuit.library import PauliTwoDesign
22-
from qiskit.opflow import I, Z, StateFn
22+
from qiskit.primitives import Sampler
23+
from qiskit.providers.basicaer import StatevectorSimulatorPy
24+
from qiskit.opflow import I, Z, StateFn, MatrixExpectation
2325
from qiskit.utils import algorithm_globals
2426

2527

@@ -195,3 +197,28 @@ def objective(x):
195197
point = np.ones(5)
196198
result = SPSA.estimate_stddev(objective, point, avg=10, max_evals_grouped=max_evals_grouped)
197199
self.assertAlmostEqual(result, 0)
200+
201+
def test_qnspsa_fidelity_deprecation(self):
202+
"""Test using a backend and expectation converter in get_fidelity warns."""
203+
ansatz = PauliTwoDesign(2, reps=1, seed=2)
204+
205+
with self.assertWarns(PendingDeprecationWarning):
206+
_ = QNSPSA.get_fidelity(ansatz, StatevectorSimulatorPy(), MatrixExpectation())
207+
208+
def test_qnspsa_fidelity_primitives(self):
209+
"""Test the primitives can be used in get_fidelity."""
210+
ansatz = PauliTwoDesign(2, reps=1, seed=2)
211+
initial_point = np.random.random(ansatz.num_parameters)
212+
213+
with self.subTest(msg="pass as kwarg"):
214+
fidelity = QNSPSA.get_fidelity(ansatz, sampler=Sampler())
215+
result = fidelity(initial_point, initial_point)
216+
217+
self.assertAlmostEqual(result[0], 1)
218+
219+
# this test can be removed once backend and expectation are removed
220+
with self.subTest(msg="pass positionally"):
221+
fidelity = QNSPSA.get_fidelity(ansatz, Sampler())
222+
result = fidelity(initial_point, initial_point)
223+
224+
self.assertAlmostEqual(result[0], 1)

0 commit comments

Comments
 (0)