Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to primitive support in QNSPSA #8682

Merged
merged 12 commits into from
Sep 30, 2022
139 changes: 122 additions & 17 deletions qiskit/algorithms/optimizers/qnspsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,20 @@

"""The QN-SPSA optimizer."""

from typing import Any, Iterator, Optional, Union, Callable, Dict
from __future__ import annotations
from collections.abc import Iterator, Callable
from typing import Any
ElePT marked this conversation as resolved.
Show resolved Hide resolved
import warnings

import numpy as np
from qiskit.providers import Backend
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
Expand Down Expand Up @@ -55,6 +61,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
Expand Down Expand Up @@ -87,18 +124,17 @@ def __init__(
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:
Expand All @@ -121,8 +157,6 @@ def __init__(
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
Expand Down Expand Up @@ -206,7 +240,7 @@ def _point_sample(self, loss, x, eps, delta1, delta2):
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
Expand All @@ -221,11 +255,82 @@ def settings(self) -> Dict[str, Any]:
@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
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
2 changes: 1 addition & 1 deletion qiskit/algorithms/state_fidelities/base_state_fidelity.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
import numpy as np

from qiskit import QuantumCircuit
from qiskit.algorithms.algorithm_job import AlgorithmJob
from qiskit.circuit import ParameterVector
from .state_fidelity_result import StateFidelityResult
from ..algorithm_job import AlgorithmJob
woodsp-ibm marked this conversation as resolved.
Show resolved Hide resolved


class BaseStateFidelity(ABC):
Expand Down
3 changes: 2 additions & 1 deletion qiskit/algorithms/state_fidelities/compute_uncompute.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
from copy import copy

from qiskit import QuantumCircuit
from qiskit.algorithms import AlgorithmError
from qiskit.primitives import BaseSampler
from qiskit.providers import Options

from .base_state_fidelity import BaseStateFidelity
from .state_fidelity_result import StateFidelityResult

from ..exceptions import AlgorithmError

woodsp-ibm marked this conversation as resolved.
Show resolved Hide resolved

class ComputeUncompute(BaseStateFidelity):
r"""
Expand Down
16 changes: 16 additions & 0 deletions releasenotes/notes/qnspsa-primitification-29a9dcae055bf2b4.yaml
Original file line number Diff line number Diff line change
@@ -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.
29 changes: 28 additions & 1 deletion test/python/algorithms/optimizers/test_spsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

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


Expand Down Expand Up @@ -195,3 +197,28 @@ def objective(x):
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)