Note

This page was generated from docs/tutorials/12_gradients_framework.ipynb.

Gradient Framework#

This tutorial demonstrates the use of the qiskit_algorithms.gradients module to evaluate quantum gradients using the Qiskit Primitives.

Introduction#

The gradient frameworks allows the evaluation of quantum gradients (see Schuld et al. and Mari et al.). Besides gradients of expectation values of the form

\[\langle\psi\left(\theta\right)|\hat{O}\left(\omega\right)|\psi\left(\theta\right)\rangle\]

and sampling probabilities of the form

\[p_j(\theta) = |\langle j | \psi(\theta)\rangle|^2\]

the gradient frameworks also support the evaluation of the Quantum Geometric Tensor (QGT) and Quantum Fisher Information (QFI) of quantum states \(|\psi\left(\theta\right)\rangle\).

A quick refresher on Qiskit Primitives#

The Qiskit Primitives work as an abstraction level between algorithms and (real/simulated) quantum devices. Instead of having to manually deal with tasks such as parameter binding or circuit transpilation, the primitives module offers a Sampler and an Estimator class that take the circuits, the observable Hamiltonians, and the circuit parameters and return the sampling distribution and the computed expectation values respectively.

qiskit.primitives provides two classes for evaluating the circuit: - The Estimator class allows to evaluate expectation values of observables with respect to states prepared by quantum circuits. - The Sampler class returns quasi-probability distributions as a result of sampling quantum circuits.

The qiskit_algorithms.gradients Framework#

The qiskit_algorithms.gradients module contains tools to compute both circuit gradients and circuit metrics. The gradients extend the BaseEstimatorGradient and BaseSamplerGradient base classes. These are abstract classes on top of which different gradient methods have been implemented. The methods currently available in this module are: - Parameter Shift Gradients - Finite Difference Gradients - Linear Combination of Unitaries Gradients - Simultaneous Perturbation Stochastic Approximation (SPSA) Gradients

Additionally, the module offers reverse gradients for efficient classical computations.

The metrics available are based on the notion of the Quantum Geometric Tensor (QGT). There is a BaseQGT class (Estimator-based) on top of which different QGT methods have been implemented: - Linear Combination of Unitaries QGT - Reverse QGT (classical) As well as a Quantum Fisher Information class (QFI) that is initialized with a reference QGT implementation from the above list.

gradients_qiskit_algorithms.png

The outline of the qiskit_algorithms.gradients framework

Gradients#

Given a parameterized quantum state \(|\psi\left(\theta\right)\rangle = V\left(\theta\right)|\psi\rangle\) with input state \(|\psi\rangle\), , we want to compute either its expectation gradient \(\langle\psi\left(\theta\right)|\hat{O}\left(\omega\right)|\psi\left(\theta\right)\rangle\) or the gradient of the sampling probability \(p_j(\theta) = |\langle j | \psi(\theta)\rangle|^2\)

Sampling gradients#

The formula for the gradient of sampling is:

\[\frac{\partial}{\partial \theta} p_j(\theta) = \frac{\partial}{\partial \theta} |\langle j | \psi(\theta)\rangle|^2\]

Thus, the output of the sampler gradient is a list of dictionaries, where each dictionary has entries for different values of \(j\) in the formula above:

[{d/d theta_1 p_1: .., d/d theta_1 p_2, ..,}, {d/d theta_2 p_1: .., d/d theta_2 p_2, ..}, ..]

Expectation gradients#

The formula for expectation gradient is:

\[\frac{\partial}{\partial \theta} \langle E \rangle = \frac{\partial}{\partial \theta} \langle\psi\left(\theta\right)|\hat{O}\left(\omega\right)|\psi\left(\theta\right)\rangle\]

Thus, the output format of the estimator gradient is a list of derivatives:

[d/d theta_1 <E>, d/d theta_2 <E>, ...]

Gradient Evaluation of Quantum Circuits#

  • Let’s say that we want to use one of our Estimator gradients classes, then we need a quantum state \(\vert\psi(\theta)\rangle\) and a Hamiltonian H acting as an observable. For the Sampler gradients, we just need a quantum state.

  • We then construct a list of the parameters for which we aim to evaluate the gradient.

[1]:
from qiskit.circuit import QuantumCircuit, QuantumRegister, Parameter
from qiskit.quantum_info import SparsePauliOp
import numpy as np

# Instantiate the quantum circuit
a = Parameter("a")
b = Parameter("b")
q = QuantumRegister(1)
qc = QuantumCircuit(q)
qc.h(q)
qc.rz(a, q[0])
qc.rx(b, q[0])

display(qc.draw("mpl"))

# Instantiate the Hamiltonian observable 2X+Z
H = SparsePauliOp.from_list([("X", 2), ("Z", 1)])

# Parameter list
params = [[np.pi / 4, 0]]
../_images/tutorials_12_gradients_framework_8_0.png

We can now choose a gradient type to evaluate the gradient of the circuit ansatz.

Parameter Shift Gradients#

Using Estimator#

Given a Hermitian operator \(g\) with two unique eigenvalues \(\pm r\) which acts as generator for a parameterized quantum gate

\[G(\theta)= e^{-i\theta g}.\]

Then, quantum gradients can be computed by using eigenvalue \(r\) dependent shifts to parameters. All standard, parameterized Qiskit gates can be shifted with \(\pi/2\), i.e.,

\[\frac{\partial\langle\psi\left(\theta\right)|\hat{O}\left(\omega\right)|\psi\left(\theta\right)\rangle}{\partial\theta} = \frac{\langle\psi\left(\theta+\pi/2\right)|\hat{O}\left(\omega\right)|\psi\left(\theta+\pi/2\right)\rangle -\langle\psi\left(\theta-\pi/2\right)|\hat{O}\left(\omega\right)|\psi\left(\theta-\pi/2\right)\rangle }{ 2}.\]
[2]:
from qiskit.primitives import Estimator
from qiskit_algorithms.gradients import ParamShiftEstimatorGradient

# Define the estimator
estimator = Estimator()
# Define the gradient
gradient = ParamShiftEstimatorGradient(estimator)

# Evaluate the gradient of the circuits using parameter shift gradients
pse_grad_result = gradient.run(qc, H, params).result().gradients

print("State estimator gradient computed with parameter shift", pse_grad_result)
State estimator gradient computed with parameter shift [array([-1.41421356,  0.70710678])]
/tmp/ipykernel_27948/3439047639.py:5: DeprecationWarning: The class ``qiskit.primitives.estimator.Estimator`` is deprecated as of qiskit 1.2. It will be removed no earlier than 3 months after the release date. All implementations of the `BaseEstimatorV1` interface have been deprecated in favor of their V2 counterparts. The V2 alternative for the `Estimator` class is `StatevectorEstimator`.
  estimator = Estimator()

Using Sampler#

Following a similar logic to the estimator gradient, when we have a quantum state prepared by a quantum circuit, we can shift the parametrized gates by \(\pm \pi/2\) and sample to compute the gradient of the sampling probability.

[3]:
# Instantiate the quantum state with two parameters
a = Parameter("a")
b = Parameter("b")

q = QuantumRegister(1)
qc_sample = QuantumCircuit(q)
qc_sample.h(q)
qc_sample.rz(a, q[0])
qc_sample.rx(b, q[0])
qc_sample.measure_all()  # important for sampler

qc_sample.draw("mpl")
[3]:
../_images/tutorials_12_gradients_framework_13_0.png
[4]:
from qiskit.primitives import Sampler
from qiskit_algorithms.gradients import ParamShiftSamplerGradient

param_vals = [[np.pi / 4, np.pi / 2]]
sampler = Sampler()
gradient = ParamShiftSamplerGradient(sampler)
pss_grad_result = gradient.run(qc_sample, param_vals).result().gradients
print("State sampler gradient computed with parameter shift", pss_grad_result)
State sampler gradient computed with parameter shift [[{0: 0.35355339059327373, 1: -0.3535533905932736}, {0: 0.0, 1: -5.551115123125783e-17}]]
/tmp/ipykernel_27948/961213124.py:5: DeprecationWarning: The class ``qiskit.primitives.sampler.Sampler`` is deprecated as of qiskit 1.2. It will be removed no earlier than 3 months after the release date. All implementations of the `BaseSamplerV1` interface have been deprecated in favor of their V2 counterparts. The V2 alternative for the `Sampler` class is `StatevectorSampler`.
  sampler = Sampler()

Note: All the following methods in this tutorial are explained using the Estimator class to evaluate the gradients, but, in an analogous way to the Parameter Shift gradients just introduced, the method explanation can also be applied to Sampler-based gradients. Both versions are available in the gradients module.

Linear Combination of Unitaries Gradients#

Unitaries can be written as \(U\left(\omega\right) = e^{iM\left(\omega\right)}\), where \(M\left(\omega\right)\) denotes a parameterized Hermitian matrix. Further, Hermitian matrices can be decomposed into weighted sums of Pauli terms, i.e., \(M\left(\omega\right) = \sum_pm_p\left(\omega\right)h_p\) with \(m_p\left(\omega\right)\in\mathbb{R}\) and \(h_p=\bigotimes\limits_{j=0}^{n-1}\sigma_{j, p}\) for \(\sigma_{j, p}\in\left\{I, X, Y, Z\right\}\) acting on the \(j^{\text{th}}\) qubit. Thus, the gradients of \(U_k\left(\omega_k\right)\) are given by

\[\frac{\partial U_k\left(\omega_k\right)}{\partial\omega_k} = \sum\limits_pi \frac{\partial m_{k,p}\left(\omega_k\right)}{\partial\omega_k}U_k\left(\omega_k\right)h_{k_p}\]

Combining this observation with a circuit structure presented in Simulating physical phenomena by quantum networks allows us to compute the gradient with the evaluation of a single quantum circuit.

[5]:
from qiskit_algorithms.gradients import LinCombEstimatorGradient

# Evaluate the gradient of the circuits using linear combination of unitaries
state_grad = LinCombEstimatorGradient(estimator)

# Evaluate the gradient
lce_grad_result = state_grad.run(qc, H, params).result().gradients
print("State estimator gradient computed with the linear combination method", lce_grad_result)
State estimator gradient computed with the linear combination method [array([-1.41421356,  0.70710678])]

Finite Difference Gradients#

Unlike the other methods, finite difference gradients are numerical estimations rather than analytical values. This implementation employs a central difference approach with \(\epsilon \ll 1\)

\[\frac{\partial\langle\psi\left(\theta\right)|\hat{O}\left(\omega\right)|\psi\left(\theta\right)\rangle}{\partial\theta} \approx \frac{1}{2\epsilon} \left(\langle\psi\left(\theta+\epsilon\right)|\hat{O}\left(\omega\right)|\psi\left(\theta+\epsilon\right)\rangle - \partial\langle\psi\left(\theta-\epsilon\right)|\hat{O}\left(\omega\right)|\psi\left(\theta-\epsilon\right)\rangle\right).\]

Probability gradients are computed equivalently.

[6]:
from qiskit_algorithms.gradients import FiniteDiffEstimatorGradient

state_grad = FiniteDiffEstimatorGradient(estimator, epsilon=0.001)

# Evaluate the gradient
fde_grad_result = state_grad.run(qc, H, params).result().gradients
print("State estimator gradient computed with finite difference", fde_grad_result)
State estimator gradient computed with finite difference [array([-1.41421333,  0.70710666])]

SPSA Gradients#

SPSA gradients compute the gradients of the expectation value by the Simultaneous Perturbation Stochastic Approximation (SPSA) algorithm. epsilon is the amount of offset, batch_size is the number of times the circuit is executed to estimate the gradient. As SPSA is a random process, use the seed value to avoid randomization.

[7]:
from qiskit_algorithms.gradients import SPSAEstimatorGradient

state_grad = SPSAEstimatorGradient(estimator, epsilon=0.001, batch_size=10, seed=50)

# Evaluate the gradient
spsae_grad_result = state_grad.run(qc, H, params).result().gradients
print("State estimator gradient computed with SPSA:", spsae_grad_result)
State estimator gradient computed with SPSA: [array([-1.41421333,  0.70710631])]

Circuit Quantum Geometric Tensor (QGTs)#

Quantum Geometric Tensor is a metric in geometric quantum computing and can be regarded as a metric measuring the geodesic distance of points lying on the Bloch sphere. Its real and imaginary parts give different information about the quantum state.

The entries of the QGT for a pure state is given by

\[QGT_{kl}(\theta) = \langle\partial_k\psi(\theta)|\partial_l\psi(\theta)\rangle-\langle\partial_k\psi(\theta)|\psi(\theta)\rangle\langle\psi(\theta)|\partial_l\psi(\theta)\rangle.\]

Linear Combination QGT#

This method employs a linear combination of unitaries, as explained in the Gradients section.

[8]:
from qiskit_algorithms.gradients import DerivativeType, LinCombQGT

qgt = LinCombQGT(estimator, derivative_type=DerivativeType.COMPLEX)

param_vals = [[np.pi / 4, 0.1]]

# Evaluate the QGTs
qgt_result = qgt.run(qc, param_vals).result().qgts
print("QGT:")
print(qgt_result)
QGT:
[array([[ 2.50000000e-01+7.54884100e-19j, -3.75686535e-17+1.76776695e-01j],
       [-3.75686535e-17-1.76776695e-01j,  1.25000000e-01-7.54884100e-19j]])]

Quantum Fisher Information (QFI)#

Quantum Fisher Information is a metric tensor which is representative for the representation capacity of a parameterized quantum state \(|\psi\left(\theta\right)\rangle = V\left(\theta\right)|\psi\rangle\) with input state \(|\psi\rangle\), parametrized Ansatz \(V\left(\theta\right)\).

The QFI can thus be evaluated from QGT as

\[\begin{split}\begin{align*} QFI_{kl} &= 4 * \text{Re}(QGT_{kl}) \\ &=4 *\text{Re}\left[\langle\partial_k\psi|\partial_l\psi\rangle-\langle\partial_k\psi|\psi\rangle\langle\psi|\partial_l\psi\rangle \right]. \end{align*}\end{split}\]
[9]:
from qiskit_algorithms.gradients import QFI

# Define the QFI metric for the QGT
qfi = QFI(qgt)

# Evaluate the QFI
qfi_result = qfi.run(qc, param_vals).result().qfis
print("QFI:")
print(qfi_result)
QFI:
[array([[ 1.00000000e+00, -1.50274614e-16],
       [-1.50274614e-16,  5.00000000e-01]])]

Application Example: VQE with gradient-based optimization#

Estimator#

Let’s see an application of these gradient classes in a gradient-based optimization. We will use the Variational Quantum Eigensolver (VQE) algorithm. First, the Hamiltonian and wavefunction ansatz are initialized.

[10]:
from qiskit.circuit import ParameterVector

# Instantiate the system Hamiltonian
h2_hamiltonian = SparsePauliOp.from_list(
    [("II", -1.05), ("IZ", 0.39), ("ZI", -0.39), ("ZZ", -0.01)]
)

# This is the target energy
h2_energy = -1.85727503

# Define the Ansatz
wavefunction = QuantumCircuit(2)
params = ParameterVector("theta", length=8)
it = iter(params)
wavefunction.ry(next(it), 0)
wavefunction.ry(next(it), 1)
wavefunction.rz(next(it), 0)
wavefunction.rz(next(it), 1)
wavefunction.cx(0, 1)
wavefunction.ry(next(it), 0)
wavefunction.ry(next(it), 1)
wavefunction.rz(next(it), 0)
wavefunction.rz(next(it), 1)

wavefunction.draw("mpl")
[10]:
../_images/tutorials_12_gradients_framework_29_0.png
[11]:
# Make circuit copies for different VQEs
wavefunction_1 = wavefunction.copy()
wavefunction_2 = wavefunction.copy()

The VQE will take an Estimator, the ansatz and optimizer, and an optional gradient. We will use the LinCombEstimatorGradient gradient to compute the VQE.

[12]:
from qiskit_algorithms.optimizers import CG
from qiskit_algorithms import VQE, SamplingVQE

# Conjugate Gradient algorithm
optimizer = CG(maxiter=50)

# Gradient callable
estimator = Estimator()
grad = LinCombEstimatorGradient(estimator)  # optional estimator gradient
vqe = VQE(estimator=estimator, ansatz=wavefunction, optimizer=optimizer, gradient=grad)

result = vqe.compute_minimum_eigenvalue(h2_hamiltonian)
print("Result of Estimator VQE:", result.optimal_value, "\nReference:", h2_energy)
Result of Estimator VQE: -1.8199999999700125
Reference: -1.85727503
/tmp/ipykernel_27948/100892596.py:8: DeprecationWarning: The class ``qiskit.primitives.estimator.Estimator`` is deprecated as of qiskit 1.2. It will be removed no earlier than 3 months after the release date. All implementations of the `BaseEstimatorV1` interface have been deprecated in favor of their V2 counterparts. The V2 alternative for the `Estimator` class is `StatevectorEstimator`.
  estimator = Estimator()

Classical Optimizer#

We can also use a classical optimizer to optimize the VQE. We’ll use the minimize function from SciPy.

[13]:
from scipy.optimize import minimize

# Classical optimizer
vqe_classical = VQE(estimator=estimator, ansatz=wavefunction_2, optimizer=minimize, gradient=grad)

result_classical = vqe_classical.compute_minimum_eigenvalue(h2_hamiltonian)
print("Result of classical optimizer:", result_classical.optimal_value, "\nReference:", h2_energy)
Result of classical optimizer: -1.8199999997816352
Reference: -1.85727503
[14]:
import tutorial_magics

%qiskit_version_table
%qiskit_copyright

Version Information

SoftwareVersion
qiskit1.2.4
qiskit_algorithms0.3.1
System information
Python version3.8.18
OSLinux
Mon Oct 21 11:16:20 2024 UTC

This code is a part of a Qiskit project

© Copyright IBM 2017, 2024.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.

Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.