Switch to primitive support in QNSPSA (#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>
This commit is contained in:
Julien Gacon 2022-09-30 08:20:15 +02:00 committed by GitHub
parent 6beab09bf2
commit 259e14e08b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 165 additions and 18 deletions

View File

@ -12,7 +12,9 @@
"""The QN-SPSA optimizer."""
from typing import Any, Iterator, Optional, Union, Callable, Dict
from __future__ import annotations
from typing import Any, Iterator, Callable
import warnings
import numpy as np
from qiskit.providers import Backend
@ -20,6 +22,9 @@ from qiskit.circuit import ParameterVector, QuantumCircuit
from qiskit.opflow import StateFn, CircuitSampler, ExpectationBase
from qiskit.utils import QuantumInstance
from qiskit.primitives import BaseSampler, Sampler
from qiskit.algorithms.state_fidelities import ComputeUncompute
from .spsa import SPSA, CALLBACK, TERMINATIONCHECKER, _batch_evaluate
# the function to compute the fidelity
@ -55,6 +60,37 @@ class QNSPSA(SPSA):
This short example runs QN-SPSA for the ground state calculation of the ``Z ^ Z``
observable where the ansatz is a ``PauliTwoDesign`` circuit.
.. code-block:: python
import numpy as np
from qiskit.algorithms.optimizers import QNSPSA
from qiskit.circuit.library import PauliTwoDesign
from qiskit.primitives import Estimator, Sampler
from qiskit.quantum_info import Pauli
# problem setup
ansatz = PauliTwoDesign(2, reps=1, seed=2)
observable = Pauli("ZZ")
initial_point = np.random.random(ansatz.num_parameters)
# loss function
estimator = Estimator()
def loss(x):
result = estimator.run([ansatz], [observable], [x]).result()
return np.real(result.values[0])
# fidelity for estimation of the geometric tensor
sampler = Sampler()
fidelity = QNSPSA.get_fidelity(ansatz, sampler)
# run QN-SPSA
qnspsa = QNSPSA(fidelity, maxiter=300)
result = qnspsa.optimize(ansatz.num_parameters, loss, initial_point=initial_point)
This is a legacy version solving the same problem but using Qiskit Opflow instead
of the Qiskit Primitives. Note however, that this usage is pending deprecation.
.. code-block:: python
import numpy as np
@ -87,18 +123,17 @@ class QNSPSA(SPSA):
fidelity: FIDELITY,
maxiter: int = 100,
blocking: bool = True,
allowed_increase: Optional[float] = None,
learning_rate: Optional[Union[float, Callable[[], Iterator]]] = None,
perturbation: Optional[Union[float, Callable[[], Iterator]]] = None,
last_avg: int = 1,
resamplings: Union[int, Dict[int, int]] = 1,
perturbation_dims: Optional[int] = None,
regularization: Optional[float] = None,
allowed_increase: float | None = None,
learning_rate: float | Callable[[], Iterator] | None = None,
perturbation: float | Callable[[], Iterator] | None = None,
resamplings: int | dict[int, int] = 1,
perturbation_dims: int | None = None,
regularization: float | None = None,
hessian_delay: int = 0,
lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None,
initial_hessian: Optional[np.ndarray] = None,
callback: Optional[CALLBACK] = None,
termination_checker: Optional[TERMINATIONCHECKER] = None,
lse_solver: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = None,
initial_hessian: np.ndarray | None = None,
callback: CALLBACK | None = None,
termination_checker: TERMINATIONCHECKER | None = None,
) -> None:
r"""
Args:
@ -121,8 +156,6 @@ class QNSPSA(SPSA):
approximation of the gradients. Can be either a float or a generator yielding
the perturbation magnitudes per step.
If ``perturbation`` is set ``learning_rate`` must also be provided.
last_avg: Return the average of the ``last_avg`` parameters instead of just the
last parameter values.
resamplings: The number of times the gradient (and Hessian) is sampled using a random
direction to construct a gradient estimate. Per default the gradient is estimated
using only one random direction. If an integer, all iterations use the same number
@ -206,7 +239,7 @@ class QNSPSA(SPSA):
return np.mean(loss_values), gradient_estimate, hessian_estimate
@property
def settings(self) -> Dict[str, Any]:
def settings(self) -> dict[str, Any]:
"""The optimizer settings in a dictionary format."""
# re-use serialization from SPSA
settings = super().settings
@ -221,11 +254,82 @@ class QNSPSA(SPSA):
@staticmethod
def get_fidelity(
circuit: QuantumCircuit,
backend: Optional[Union[Backend, QuantumInstance]] = None,
expectation: Optional[ExpectationBase] = None,
backend: Backend | QuantumInstance | None = None,
expectation: ExpectationBase | None = None,
*,
sampler: BaseSampler | None = None,
) -> Callable[[np.ndarray, np.ndarray], float]:
r"""Get a function to compute the fidelity of ``circuit`` with itself.
.. note::
Using this function with a backend and expectation converter is pending deprecation,
instead pass a Qiskit Primitive sampler, such as :class:`~.Sampler`.
The sampler can be passed as keyword argument or, positionally, as second argument.
Let ``circuit`` be a parameterized quantum circuit performing the operation
:math:`U(\theta)` given a set of parameters :math:`\theta`. Then this method returns
a function to evaluate
.. math::
F(\theta, \phi) = \big|\langle 0 | U^\dagger(\theta) U(\phi) |0\rangle \big|^2.
The output of this function can be used as input for the ``fidelity`` to the
:class:~`qiskit.algorithms.optimizers.QNSPSA` optimizer.
Args:
circuit: The circuit preparing the parameterized ansatz.
backend: *Pending deprecation.* A backend of quantum instance to evaluate the circuits.
If None, plain matrix multiplication will be used.
expectation: *Pending deprecation.* An expectation converter to specify how the expected
value is computed. If a shot-based readout is used this should be set to
``PauliExpectation``.
sampler: A sampler primitive to sample from a quantum state.
Returns:
A handle to the function :math:`F`.
"""
# allow passing sampler by position
if isinstance(backend, BaseSampler):
sampler = backend
backend = None
if expectation is None and backend is None and sampler is None:
sampler = Sampler()
if expectation is not None or backend is not None:
warnings.warn(
"Passing a backend and expectation converter to QNSPSA.get_fidelity is pending "
"deprecation and will be deprecated in a future release. Instead, pass a "
"sampler primitive.",
stacklevel=2,
category=PendingDeprecationWarning,
)
return QNSPSA._legacy_get_fidelity(circuit, backend, expectation)
fid = ComputeUncompute(sampler)
def fidelity(values_x, values_y):
result = fid.run(circuit, circuit, values_x, values_y).result()
return np.asarray(result.fidelities)
return fidelity
@staticmethod
def _legacy_get_fidelity(
circuit: QuantumCircuit,
backend: Backend | QuantumInstance | None = None,
expectation: ExpectationBase | None = None,
) -> Callable[[np.ndarray, np.ndarray], float]:
r"""PENDING DEPRECATION. Get a function to compute the fidelity of ``circuit`` with itself.
.. note::
This method is pending deprecation. Instead use the :class:`~.ComputeUncompute`
class which implements the fidelity calculation in the same fashion as this method.
Let ``circuit`` be a parameterized quantum circuit performing the operation
:math:`U(\theta)` given a set of parameters :math:`\theta`. Then this method returns
a function to evaluate

View File

@ -0,0 +1,16 @@
---
features:
- |
Add support for the :class:`.BaseSampler` primitive in :func:`.QNSPSA.get_fidelity`.
Now, the fidelity function can be constructed as::
from qiskit.primitives import Sampler
from qiskit.algorithms.optimizers import QNSPSA
fidelity = QNSPSA.get_fidelity(my_circuit, Sampler())
deprecations:
- |
Using a backend and expectation converter in :func:`.QNSPSA.get_fidelity` is
pending deprecation and will be deprecated in a future release. Instead, use
a :class:`.BaseSampler` to evaluate circuits, see also the features of this release.

View File

@ -19,7 +19,9 @@ import numpy as np
from qiskit.algorithms.optimizers import SPSA, QNSPSA
from qiskit.circuit.library import PauliTwoDesign
from qiskit.opflow import I, Z, StateFn
from qiskit.primitives import Sampler
from qiskit.providers.basicaer import StatevectorSimulatorPy
from qiskit.opflow import I, Z, StateFn, MatrixExpectation
from qiskit.utils import algorithm_globals
@ -195,3 +197,28 @@ class TestSPSA(QiskitAlgorithmsTestCase):
point = np.ones(5)
result = SPSA.estimate_stddev(objective, point, avg=10, max_evals_grouped=max_evals_grouped)
self.assertAlmostEqual(result, 0)
def test_qnspsa_fidelity_deprecation(self):
"""Test using a backend and expectation converter in get_fidelity warns."""
ansatz = PauliTwoDesign(2, reps=1, seed=2)
with self.assertWarns(PendingDeprecationWarning):
_ = QNSPSA.get_fidelity(ansatz, StatevectorSimulatorPy(), MatrixExpectation())
def test_qnspsa_fidelity_primitives(self):
"""Test the primitives can be used in get_fidelity."""
ansatz = PauliTwoDesign(2, reps=1, seed=2)
initial_point = np.random.random(ansatz.num_parameters)
with self.subTest(msg="pass as kwarg"):
fidelity = QNSPSA.get_fidelity(ansatz, sampler=Sampler())
result = fidelity(initial_point, initial_point)
self.assertAlmostEqual(result[0], 1)
# this test can be removed once backend and expectation are removed
with self.subTest(msg="pass positionally"):
fidelity = QNSPSA.get_fidelity(ansatz, Sampler())
result = fidelity(initial_point, initial_point)
self.assertAlmostEqual(result[0], 1)