qml.gradients

Quantum gradient transforms are strategies for computing the gradient of a quantum circuit that work by transforming the quantum circuit into one or more gradient circuits. These gradient circuits, once executed and post-processed, return the gradient of the original circuit.

Examples of quantum gradient transforms include finite-differences and parameter-shift rules.

This module provides a selection of device-independent, differentiable quantum gradient transforms. As such, these quantum gradient transforms can be used to compute the gradients of quantum circuits on both simulators and hardware.

In addition, it also includes an API for writing your own quantum gradient transforms.

These quantum gradient transforms can be used in two ways:

  • Transforming quantum circuits directly

  • Registering a quantum gradient strategy for use when performing autodifferentiation with a QNode.

Overview

Gradient transforms

finite_diff(tape[, argnum, h, approx_order, ...])

Transform a circuit to compute the finite-difference gradient of all gate parameters with respect to its inputs.

param_shift(tape[, argnum, shifts, ...])

Transform a circuit to compute the parameter-shift gradient of all gate parameters with respect to its inputs.

param_shift_cv(tape, dev[, argnum, shifts, ...])

Transform a continuous-variable QNode to compute the parameter-shift gradient of all gate parameters with respect to its inputs.

param_shift_hessian(tape[, argnum, ...])

Transform a circuit to compute the parameter-shift Hessian with respect to its trainable parameters.

spsa_grad(tape[, argnum, h, approx_order, ...])

Transform a circuit to compute the SPSA gradient of all gate parameters with respect to its inputs.

hadamard_grad(tape[, argnum, aux_wire, ...])

Transform a circuit to compute the Hadamard test gradient of all gates with respect to their inputs.

stoch_pulse_grad(tape[, argnum, ...])

Compute the gradient of a quantum circuit composed of pulse sequences by applying the stochastic parameter shift rule.

pulse_odegen(tape[, argnum, atol])

Transform a circuit to compute the pulse generator parameter-shift gradient of pulses in a pulse program with respect to their inputs.

Metric tensors

metric_tensor(tape[, argnum, approx, ...])

Returns a function that computes the metric tensor of a given QNode or quantum tape.

adjoint_metric_tensor(tape)

Implements the adjoint method outlined in Jones to compute the metric tensor.

Utility functions

finite_diff_coeffs(n, approx_order, strategy)

Generate the finite difference shift values and corresponding term coefficients for a given derivative order, approximation accuracy, and strategy.

generate_shifted_tapes(tape, index, shifts)

Generate a list of tapes or a single broadcasted tape, where one marked trainable parameter has been shifted by the provided shift values.

generate_multishifted_tapes(tape, indices, ...)

Generate a list of tapes where multiple marked trainable parameters have been shifted by the provided shift values.

generate_shift_rule(frequencies[, shifts, order])

Computes the parameter shift rule for a unitary based on its generator's eigenvalue frequency spectrum.

generate_multi_shift_rule(frequencies[, ...])

Computes the parameter shift rule with respect to two parametrized unitaries, given their generator's eigenvalue frequency spectrum.

eigvals_to_frequencies(eigvals)

Convert an eigenvalue spectrum to frequency values, defined as the the set of positive, unique differences of the eigenvalues in the spectrum.

compute_vjp_single(dy, jac[, num])

Convenience function to compute the vector-Jacobian product for a given vector of gradient outputs and a Jacobian for a single measurement tape.

compute_vjp_multi(dy, jac[, num])

Convenience function to compute the vector-Jacobian product for a given vector of gradient outputs and a Jacobian for a tape with multiple measurements.

batch_vjp(tapes, dys, gradient_fn[, ...])

Generate the gradient tapes and processing function required to compute the vector-Jacobian products of a batch of tapes.

vjp(tape, dy, gradient_fn[, gradient_kwargs])

Generate the gradient tapes and processing function required to compute the vector-Jacobian products of a tape.

compute_jvp_single(tangent, jac)

Convenience function to compute the Jacobian vector product for a given tangent vector and a Jacobian for a single measurement tape.

compute_jvp_multi(tangent, jac)

Convenience function to compute the Jacobian-vector product for a given vector of gradient outputs and a Jacobian for a tape with multiple measurements.

batch_jvp(tapes, tangents, gradient_fn[, ...])

Generate the gradient tapes and processing function required to compute the Jacobian vector products of a batch of tapes.

jvp(tape, tangent, gradient_fn[, ...])

Generate the gradient tapes and processing function required to compute the Jacobian vector product of a tape.

classical_jacobian(qnode[, argnum, ...])

Returns a function to extract the Jacobian matrix of the classical part of a QNode.

classical_fisher(qnode[, argnums])

Returns a function that computes the classical fisher information matrix (CFIM) of a given QNode or quantum tape.

quantum_fisher(tape, device, *args, **kwargs)

Returns a function that computes the quantum fisher information matrix (QFIM) of a given QNode.

Registering autodifferentiation gradients

All PennyLane QNodes are automatically differentiable, and can be included seamlessly within an autodiff pipeline. When creating a QNode, the strategy for determining the optimal differentiation strategy is automated, and takes into account the circuit, device, autodiff framework, and metadata (such as whether a finite number of shots are used).

dev = qml.device("default.qubit", shots=1000)

@qml.qnode(dev, interface="tf")
def circuit(weights):
    ...

In particular:

  • When using a simulator device with exact measurement statistics, backpropagation is preferred due to performance and memory improvements.

  • When using a hardware device, or a simulator with a finite number of shots, a quantum gradient transform—such as the parameter-shift rule—is preferred.

If you would like to specify a particular quantum gradient transform to use when differentiating your quantum circuit, this can be passed when creating the QNode:

@qml.qnode(dev, diff_method=qml.gradients.param_shift)
def circuit(weights):
    ...

When using your preferred autodiff framework to compute the gradient of your hybrid quantum-classical cost function, the specified gradient transform for each QNode will be used.

Note

A single cost function may include multiple QNodes, each with their own quantum gradient transform registered.

Transforming QNodes

Alternatively, quantum gradient transforms can be applied manually to QNodes. This is not recommended because PennyLane must compute the classical Jacobian of the parameters and multiply it with the quantum Jacobian, we recommend using the diff_method kwargs with your favorite machine learning framework.

dev = qml.device("default.qubit")

@qml.qnode(dev)
def circuit(weights):
    qml.RX(weights[0], wires=0)
    qml.RY(weights[1], wires=1)
    qml.CNOT(wires=[0, 1])
    qml.RX(weights[2], wires=1)
    return qml.probs(wires=1)
>>> weights = np.array([0.1, 0.2, 0.3], requires_grad=True)
>>> circuit(weights)
tensor([0.9658079, 0.0341921], requires_grad=True)
>>> qml.gradients.param_shift(circuit)(weights)
tensor([[-0.04673668, -0.09442394, -0.14409127],
        [ 0.04673668,  0.09442394,  0.14409127]], requires_grad=True)

Comparing this to autodifferentiation:

>>> qml.jacobian(circuit)(weights)
array([[-0.04673668, -0.09442394, -0.14409127],
       [ 0.04673668,  0.09442394,  0.14409127]])

Quantum gradient transforms can also be applied as decorators to QNodes, if only gradient information is needed. Evaluating the QNode will then automatically return the gradient:

dev = qml.device("default.qubit")

@qml.gradients.param_shift
@qml.qnode(dev)
def decorated_circuit(weights):
    qml.RX(weights[0], wires=0)
    qml.RY(weights[1], wires=1)
    qml.CNOT(wires=[0, 1])
    qml.RX(weights[2], wires=1)
    return qml.probs(wires=1)
>>> decorated_circuit(weights)
tensor([[-0.04673668, -0.09442394, -0.14409127],
        [ 0.04673668,  0.09442394,  0.14409127]], requires_grad=True)

Note

If your circuit contains any operations not supported by the gradient transform, the transform will attempt to automatically decompose the circuit into only operations that support gradients.

Note

If you wish to only return the purely quantum component of the gradient—that is, the gradient of the output with respect to gate arguments, not QNode arguments—pass hybrid=False when applying the transform:

>>> qml.gradients.param_shift(circuit, hybrid=False)(weights)
(tensor([-0.04673668,  0.04673668], requires_grad=True),
 tensor([-0.09442394,  0.09442394], requires_grad=True),
 tensor([-0.14409127,  0.14409127], requires_grad=True))

Differentiating gradient transforms and higher-order derivatives

Gradient transforms are themselves differentiable, allowing higher-order gradients to be computed:

dev = qml.device("default.qubit")

@qml.qnode(dev)
def circuit(weights):
    qml.RX(weights[0], wires=0)
    qml.RY(weights[1], wires=1)
    qml.CNOT(wires=[0, 1])
    qml.RX(weights[2], wires=1)
    return qml.expval(qml.Z(1))
>>> weights = np.array([0.1, 0.2, 0.3], requires_grad=True)
>>> circuit(weights)
tensor(0.9316158, requires_grad=True)
>>> qml.gradients.param_shift(circuit)(weights)  # gradient
tensor([-0.09347337, -0.18884787, -0.28818254], requires_grad=True)
>>> def stacked_output(weights):
...     return qml.numpy.stack(qml.gradients.param_shift(circuit)(weights))
>>> qml.jacobian(stacked_output)(weights)  # hessian
array([[-0.9316158 ,  0.01894799,  0.0289147 ],
       [ 0.01894799, -0.9316158 ,  0.05841749],
       [ 0.0289147 ,  0.05841749, -0.9316158 ]])

Another way to compute higher-order derivatives is by passing the max_diff and diff_method arguments to the QNode and by successive differentiation:

@qml.qnode(dev, diff_method="parameter-shift", max_diff=2)
def circuit(weights):
    qml.RX(weights[0], wires=0)
    qml.RY(weights[1], wires=1)
    qml.CNOT(wires=[0, 1])
    qml.RX(weights[2], wires=1)
    return qml.expval(qml.Z(1))
>>> weights = np.array([0.1, 0.2, 0.3], requires_grad=True)
>>> qml.jacobian(qml.jacobian(circuit))(weights)  # hessian
array([[-0.9316158 ,  0.01894799,  0.0289147 ],
       [ 0.01894799, -0.9316158 ,  0.05841749],
       [ 0.0289147 ,  0.05841749, -0.9316158 ]])

Note that the max_diff argument only applies to gradient transforms and that its default value is 1; failing to set its value correctly may yield incorrect results for higher-order derivatives. Also, passing diff_method="parameter-shift" is equivalent to passing diff_method=qml.gradients.param_shift.

Transforming tapes

Gradient transforms can be applied to low-level QuantumTape objects, a datastructure representing variational quantum algorithms:

weights = np.array([0.1, 0.2, 0.3], requires_grad=True)

ops = [
    qml.RX(weights[0], wires=0),
    qml.RY(weights[1], wires=1),
    qml.CNOT(wires=[0, 1]),
    qml.RX(weights[2], wires=1)]
measurements = [qml.expval(qml.Z(1))]
tape = qml.tape.QuantumTape(ops, measurements)

Unlike when transforming a QNode, transforming a tape directly will perform no implicit quantum device evaluation. Instead, it returns the processed tapes, and a post-processing function, which together define the gradient:

>>> gradient_tapes, fn = qml.gradients.param_shift(tape)
>>> gradient_tapes
[<QuantumTape: wires=[0, 1], params=3>,
 <QuantumTape: wires=[0, 1], params=3>,
 <QuantumTape: wires=[0, 1], params=3>,
 <QuantumTape: wires=[0, 1], params=3>,
 <QuantumTape: wires=[0, 1], params=3>,
 <QuantumTape: wires=[0, 1], params=3>]

This can be useful if the underlying circuits representing the gradient computation need to be analyzed.

The output tapes can then be evaluated and post-processed to retrieve the gradient:

>>> dev = qml.device("default.qubit")
>>> fn(qml.execute(gradient_tapes, dev, None))
(tensor(-0.09347337, requires_grad=True),
 tensor(-0.18884787, requires_grad=True),
 tensor(-0.28818254, requires_grad=True))

Note that the post-processing function fn returned by the gradient transform is applied to the flat list of results returned from executing the gradient tapes.

Custom gradient transforms

Using the qml.transform decorator, custom gradient transforms can be created:

from pennylane.tape import QuantumScriptBatch
from pennylane.typing import PostprocessingFn

@transform
def my_custom_gradient(tape: qml.tape.QuantumScript, **kwargs) -> tuple[QuantumScriptBatch, PostprocessingFn]:
    ...
    return gradient_tapes, processing_fn

Once created, a custom gradient transform can be applied directly to QNodes, or registered as the quantum gradient transform to use during autodifferentiation.

For more details, please see the qml.transform documentation.