PennyLane v0.20 released

PennyLane team

The latest release of PennyLane is now out and available for everyone to use. It comes with many new additions, including a new graphical circuit drawer, new quantum-aware optimizers, faster performance, smarter circuit decompositions, general hardware gradient support, and more.

Check out the table of contents below, or keep reading to find out more.

Shiny new circuit drawer! πŸŽ¨πŸ–ŒοΈ

The PennyLane circuit drawer has received a makeover πŸ’„ In addition to our existing text-based circuit drawer, you can now draw your QNode in full graphical glory with qml.draw_mpl():

dev = qml.device("default.qubit", wires=4)

def circuit(x, z):
    qml.RX(x, wires=0)
    qml.CRZ(z, wires=(3,0))
    return qml.expval(qml.PauliZ(0))

fig, ax = qml.draw_mpl(circuit)(1.2345, 1.2345)

For more details and examples, please refer to the qml.draw_mpl documentation. You can also view all available circuit styles in the qml.drawer module.

New and improved quantum-aware optimizers πŸ“‰

When it comes to building variational algorithms, designing your quantum circuit and embedding is only half the battle — care needs to also be taken to choose an optimization method that improves the speed of convergence while minimizing the number of quantum evaluations required!

Quantum-aware optimizers generally provide the best of both worlds here; taking into account the geometry of the quantum landscape to both improve convergence while reducing quantum resources.

With this release, PennyLane introduces a brand new quantum-aware optimizer, qml.LieAlgebraOptimizer, alongside other optimizer improvements.

Perform gradient descent on the special unitary group

Riemannian gradient descent algorithms can be used to optimize a function directly on a Lie group as opposed to on a Euclidean parameter space.

Extending this to QNodes, qml.LieAlgebraOptimizer is a new quantum-aware Lie Algebra optimizer that allows one to perform gradient descent directly on the special unitary group:

dev = qml.device("default.qubit", wires=2)
H = -1.0 * qml.PauliX(0) - qml.PauliZ(1) - qml.PauliY(0) @ qml.PauliX(1)

def circuit():
    qml.RX(0.1, wires=[0])
    qml.RY(0.5, wires=[1])
    qml.RY(0.6, wires=[0])
    return qml.expval(H)

opt = qml.LieAlgebraOptimizer(circuit=circuit, stepsize=0.1)

Note that, unlike other optimizers, the LieAlgebraOptimizer accepts a QNode with no parameters, and instead grows the circuit by appending operations during the optimization:

>>> circuit()
tensor(-1.3351865, requires_grad=True)
>>> circuit1, cost = opt.step_and_cost()
>>> circuit1()
tensor(-1.99378872, requires_grad=True)
>>> qml.draw(circuit1, expansion_strategy='device')()
 0: ──RX(0.1)──╭C──RY(0.6)───────RZ(0.0634)──╭RZ(5.55e-18)────────────────────────────────────────────────────────────╭RZ(0.142)───────────────╭RZ(-0.0787)──H──────────RZ(-0.0787)──H──RX(1.57)──RZ(0.164)──RX(-1.57)──RX(1.57)──╭RZ(0.145)──RX(-1.57)──H──╭RZ(-0.179)──H──H─────────╭RZ(-0.0539)──H──────────RX(1.57)──╭RZ(0.0787)──RX(-1.57)──RX(1.57)──╭RZ(0.205)──RX(-1.57)──╭─ ⟨Hamiltonian(-1, -1, -1)⟩
 1: ──RY(0.5)──╰X──RZ(2.78e-17)──────────────╰RZ(5.55e-18)──H──RZ(-3.05e-17)──H──RX(1.57)──RZ(-0.0959)──RX(-1.57)──H──╰RZ(0.142)──H──RX(1.57)──╰RZ(-0.0787)──RX(-1.57)────────────────────────────────────────────────────────────╰RZ(0.145)──H─────────────╰RZ(-0.179)──H──RX(1.57)──╰RZ(-0.0539)──RX(-1.57)──H─────────╰RZ(0.0787)──H──────────RX(1.57)──╰RZ(0.205)──RX(-1.57)──╰─ ⟨Hamiltonian(-1, -1, -1)⟩

For more details, see the qml.LieAlgebraOptimizer documentation.

Improved quantum natural gradient support

The qml.metric_tensor transform, used to perform quantum natural gradient optimization via qml.QNGOptimizer, can now be used to compute the full tensor on hardware, beyond the block diagonal approximation.

This is performed using a combination of Hadamard tests and covariance matrix computations, minimizing the number of quantum executions required, while requiring an additional wire on the device.

dev = qml.device("default.qubit", wires=3)

def circuit(weights):
    qml.RX(weights[0], wires=0)
    qml.RY(weights[1], wires=0)
    qml.CNOT(wires=[0, 1])
    qml.RZ(weights[2], wires=1)
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))

weights = np.array([0.2, 1.2, -0.9], requires_grad=True)
>>> qml.metric_tensor(circuit)(weights)
[[ 0.25        0.         -0.23300977]
[ 0.          0.24013262  0.01763859]
[-0.23300977  0.01763859  0.21846983]]
>>> print(qml.draw(qml.metric_tensor(circuit))(weights))
 0: ──H──╭─ Probs
 1: ─────╰─ Probs

 0: ──RX(0.2)──Z──S──H──╭─ Probs
 1: ────────────────────╰─ Probs

 0: ──RX(0.2)──RY(1.2)──╭C──╭─ Probs
 1: ────────────────────╰X──╰─ Probs

 0: ─────╭X──RX(0.2)──╭Y───
 2: ──H──╰C───────────╰C─── ⟨X⟩

 0: ─────╭X──RX(0.2)──RY(1.2)──╭C───────
 1: ─────│─────────────────────╰X──╭Z───
 2: ──H──╰C────────────────────────╰C─── ⟨X⟩

 0: ──RX(0.2)──╭Y──RY(1.2)──╭C───────
 1: ───────────│────────────╰X──╭Z───
 2: ──H────────╰C───────────────╰C─── ⟨X⟩

Here, three 2-wire circuits have been used to compute the block-diagonal metric tensor elements, with two 3-wire circuits required to compute the off-block-diagonal elements.

As always, the full metric tensor remains fully differentiable:

>>> def cost(weights):
...     mt = qml.metric_tensor(circuit)(weights)
...     return np.linalg.norm(mt)
>>> cost(weights)
>>> qml.grad(cost)(weights)
array([-0.03351384,  0.1444742 ,  0.        ])

Characterize your quantum models with classical QNode reconstruction πŸ‘¨β€πŸ”¬

Being able to characterize your variational circuits provides valuable insight when building quantum models, and the qml.fourier module provides a smorgasbord of introspection.

New to this release is qml.fourier.reconstruct, which returns a classical function that exactly reconstructs a QNode along a specified parameter dimension, by sampling the original QNode an optimum number of times.

dev = qml.device("default.qubit", wires=2)

def circuit(x, y):
    qml.RX(x, wires=0)
    qml.RY(y[0], wires=0)
    qml.RY(y[1], wires=1)
    qml.CNOT(wires=[0, 1])
    qml.RY(y[1], wires=1)
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))

We can use qml.fourier.qnode_spectrum to compute the frequency spectrum of this QNode with respect to argument x, and use this to compute the classical reconstruction:

>>> x = 0.3
>>> y = np.array([0.1, -0.9])
>>> spectra = qml.fourier.qnode_spectrum(circuit, encoding_args={"x"})(x, y)
>>> univariate_x = qml.fourier.reconstruct(circuit, spectra=spectra)(x, y)["x"][()]

The returned reconstruction is exact and purely classical, and can be evaluated and differentiated without any quantum executions.

>>> circuit(x + 0.4, y)  # will evaluate the quantum device
>>> univariate_x(x + 0.4)  # purely classical, no quantum executions

For more details on usage, reconstruction cost and differentiability support, please see the qml.fourier.reconstruct docstring.

Faster performance with optimized quantum workflows πŸš—

The QNode has been re-written from the ground up, to support batch execution across the board, custom gradients, better decomposition strategies, and higher-order derivatives.

Note that the old QNode remains accessible at @qml.qnode_old.qnode, however this will be removed in the next release.

Batch execute quantum circuits

Internally, if multiple circuits are generated for simultaneous execution, they will be packaged into a single job for execution on the device. This can lead to significant performance improvement when executing the QNode on remote quantum hardware or simulator devices with parallelization capabilities.

\(n\)th order derivatives on hardware

Arbitrary \(n\)-th order derivatives are supported on hardware using gradient transforms such as the parameter-shift rule. To specify that an \(n\)-th order derivative of a QNode will be computed, the max_diff argument should be set. By default, this is set to 1 (first-order derivatives only).

Increasing this value allows for higher order derivatives to be extracted, at the cost of additional (classical) computational overhead during the backwards pass.

Smarter circuit decomposition strategies

When decomposing the circuit, the default decomposition strategy expansion_strategy="gradient" will prioritize decompositions that result in the smallest number of parametrized operations required to satisfy the differentiation method.

While this may lead to a slight increase in classical processing, it significantly reduces the number of circuit evaluations needed to compute gradients of complicated unitaries.

To return to the old behaviour, expansion_strategy="device" can be specified.

Support for TensorFlow AutoGraph mode with quantum hardware

It is now possible to use TensorFlow’s AutoGraph mode with QNodes on all devices and with arbitrary differentiation methods. Previously, AutoGraph mode only supported diff_method="backprop". This will result in significantly more performant model execution, at the cost of a more expensive initial compilation.

Use AutoGraph to convert your QNodes or cost functions into TensorFlow graphs by decorating them with @tf.function:

dev = qml.device("lightning.qubit", wires=2)

@qml.qnode(dev, diff_method="adjoint", interface="tf", max_diff=1)
def circuit(x):
    qml.RX(x[0], wires=0)
    qml.RY(x[1], wires=1)
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)), qml.expval(qml.PauliZ(0))

def cost(x):
    return tf.reduce_sum(circuit(x))

x = tf.Variable([0.5, 0.7], dtype=tf.float64)

with tf.GradientTape() as tape:
    loss = cost(x)

grad = tape.gradient(loss, x)

The initial execution may take slightly longer than when executing the circuit in eager mode; this is because TensorFlow is tracing the function to create the graph. Subsequent executions will be much more performant.

Note that using AutoGraph with backprop-enabled devices, such as default.qubit, will yield the best performance.

For more details, please see the TensorFlow AutoGraph documentation.

Hardware gradients of arbitrary operations πŸš£

The parameter-shift rule is used in PennyLane to support analytic derivatives of QNodes on hardware. However, up to now, it has only supported a limited gate set, including single-qubit rotations and controlled rotations, necessitating lengthy decompositions when working with high-level templates and ansΓ€tze.

With this release, we introduce qml.gradients.generate_shift_rule for computing parameter-shift rules for arbitrary operations; this avoids the need for costly decompositions, and can significantly reduce the number of quantum executions required.

Given an operator of the form \(U=e^{iHt}\), \(H = \sum_i a_i h_i\), where the eigenvalues of \(H\) are known and all \(h_i\) commute, we can compute the frequencies (the unique positive differences of any two eigenvalues) using qml.gradients.eigvals_to_frequencies.

For example, consider the case where \(H\) has eigenspectrum (-1, 0, 1):

>>> frequencies = qml.gradients.eigvals_to_frequencies((-1, 0, 1))
>>> frequencies
(1, 2)

qml.gradients.generate_shift_rule can then be used to compute parameter shift rules to compute \(\partial^n f(t)\) using shifted cost function evaluations:

>>> coeffs, shifts = qml.gradients.generate_shift_rule(frequencies, order=1)
>>> coeffs
array([ 0.85355339, -0.85355339, -0.14644661,  0.14644661])
>>> shifts
array([ 0.78539816, -0.78539816,  2.35619449, -2.35619449])

This becomes cheaper than the standard application of the chain rule and two-term shift rule when the number of frequencies is less than the number of Pauli words in the generator.

For more details, including for generating \(n\)th order partial derivatives, see the documentation for generate_shift_rule and generate_multi_shift_rule.

Define device-specific custom decompositions πŸ“Ό

By passing the custom_decomps keyword argument when loading a device, custom operation decompositions can be registered with the device:

def custom_cnot(wires):
    qml.CZ(wires=[wires[0], wires[1]])

def custom_hadamard(wires):
    qml.RZ(np.pi, wires=wires)
    qml.RY(np.pi / 2, wires=wires)

custom_decomps = {qml.CNOT : custom_cnot, "Hadamard" : custom_hadamard}
dev = qml.device("default.qubit", wires=3, custom_decomps=custom_decomps)

QNodes executed with this device will attempt to satisfy both the custom decomposition and the native device gate set, if possible:

>>> @qml.qnode(dev)
>>> def circuit(weights):
...     qml.BasicEntanglerLayers(weights, wires=[0, 1, 2])
...     return qml.expval(qml.PauliZ(0))
>>> weights = np.array([[0.4, 0.5, 0.6]])
>>> print(qml.draw(circuit, expansion_strategy="device")(weights))
 0: ──RX(0.4)──────────────────────╭C──RZ(3.14)──RY(1.57)──────────────────────────╭Z──RZ(3.14)──RY(1.57)─── ⟨Z⟩
 1: ──RX(0.5)──RZ(3.14)──RY(1.57)──╰Z──RZ(3.14)──RY(1.57)──╭C──────────────────────│────────────────────────
 2: ──RX(0.6)──RZ(3.14)──RY(1.57)──────────────────────────╰Z──RZ(3.14)──RY(1.57)──╰C───────────────────────

A separate context manager, qml.transforms.set_decomposition, is also available to enable application of custom decompositions on devices that have already been created.

New operations, templates, and transforms πŸ€–

Alongside the great new features above, we also have a ton of new operations, templates, and transforms to share. These include:

  • qml.CommutingEvolution: A circuit template for time evolution under a commuting Hamiltonian. This template utilizes generalized parameter-shift rules to greatly minimize the number of shifted circuits that must be evaluated for gradient computation.
  • qml.Barrier: Useful for separating blocks of operations during compilation, or for visualizing quantum circuits.
  • qml.PauliError: Pauli operator error channel for an arbitrary number of qubits.


In addition to the new features listed above, the release contains a wide array of improvements and optimizations:

  • Methods atomic_orbital and molecular_orbital have been added to the qml.hf.Molecule class for computing the values of atomic and molecular orbitals at a given position.
  • The PennyLane qchem package is now lazily imported; it will only be imported the first time it is accessed.
  • More templates now support the @qml.batch_params decorator, allowing them to be evaluated on hardware with parameters that include a batch dimension. These include qml.AngleEmbedding, qml.BasicEntanglerLayers, and qml.MottonenStatePreparation.
  • The text circuit drawer qml.draw() now supports a max_length argument to help prevent text overflows when printing a circuit in a terminal.
  • qml.Identity can now be used as a circuit operation, and is no longer restricted to measurements.

For the full list of improvements, please refer to the full release notes.

Breaking changes

As new things are added, outdated features are removed. Here’s what will be disappearing in this release.

  • The qml.template decorator has been removed. If the list of operations needs to be extracted from a quantum function, please use the qml.tape.QuantumTape context manager instead.
  • The deprecated default.tensor and experimental devices have been removed.
  • The qml.fourier.spectrum function has been removed (split into qml.fourier.qnode_spectrum and qml.fourier.circuit_spectrum).
  • The diag_approx keyword argument of qml.metric_tensor and qml.QNGOptimizer has been removed. The approx="block-diag"|"diag"|None keyword argument should be used instead.
  • The qml.init module, which contains functions to generate random parameters for templates, has been removed. Instead, the templates provide a shape() method.
  • The par_domain attribute in the operator class was no longer used internally, and has been removed.
  • The mutable keyword argument has been removed from the QNode, due to underlying buggy behaviour resulting in incorrect numerical results being returned during evaluation.
  • The reversible QNode differentiation method has been temporarily removed.
  • The DiagonalOperation subclass of Operator has been removed. Instead, devices can check for the diagonal property using attributes; op in qml.ops.qubit.attributes.diagonal_in_z_basis. Custom operations can be added to this attribute at runtime via diagonal_in_z_basis.add("MyCustomOp").

Other breaking changes include:

  • When drawing QNodes, the default behaviour is to expand all operations to satisfy the gradient method. The old behaviour — drawing QNodes using the native device gate set — can be returned using qml.draw(circuit, expansion_strategy="device").
  • By default, QNodes only support first derivatives. Second (and higher) derivative support can be activated by passing max_diff=2 to the QNode decorator.
  • The default behaviour of the qml.metric_tensor transform has been modified: By default, the full metric tensor is computed, leading to higher cost than the previous default of computing the block diagonal only. At the same time, the Hadamard tests for the full metric tensor require an additional wire on the device, so that qml.metric_tensor(some_qnode)(weights) will revert back to the block diagonal restriction and raise a warning if the used device does not have an additional wire.
  • The num_params attribute in the operator class is now dynamic. This makes it easier to define operator subclasses with a flexible number of parameters.
  • QuantumTape.trainable_params now is a list instead of a set.

These highlights are just scratching the surface — check out the full release notes for more details.


As always, this release would not have been possible without the hard work of our development team and contributors:

Catalina Albornoz, Guillermo Alonso-Linaje, Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Samuel Banning, Benjamin Cordier, Alain Delgado, Olivia Di Matteo, Anthony Hayes, David Ittah, Josh Izaac, Soran Jahangiri, Jalani Kanem, Ankit Khandelwal, Nathan Killoran, Shumpei Kobayashi, Robert Lang, Christina Lee, Cedric Lin, Alejandro Montanez, Romain Moyard, Lee James O’Riordan, Chae-Yeun Park, Isidor Schoch, Maria Schuld, Jay Soni, Antal SzΓ‘va, Rodrigo Vargas, David Wierichs, Roeland Wiersema, Moritz Willmann.