The latest release of PennyLane is now out and available for everyone to use. It comes with many new additions, including methods to reduce qubit counts, experimental Qiskit Runtime support, improved quantum aware optimizers, better JAX support, new transforms, templates, and more.
Check out the table of contents below, or keep reading to find out more.
Reduce qubit counts with Hamiltonian tapering 🔽
As we explore larger and larger models, the quantum data we work with — such as molecular Hamiltonians — require more qubits to be accurately represented and embedded in our quantum circuits.
With this release, PennyLane introduces qubit tapering
via qml.hf.transform_hamiltonian
.
Take advantage of inherent symmetries of fermionic Hamiltonians to reduce the number of qubits required to represent the Hamiltonian — all while preserving the original structure of the Hamiltonian.
# molecular geometry
symbols = ["He", "H"]
geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4588684632]])
mol = qml.hf.Molecule(symbols, geometry, charge=1)
# generate the qubit Hamiltonian
H = qml.hf.generate_hamiltonian(mol)(geometry)
# determine Hamiltonian symmetries
generators, paulix_ops = qml.hf.generate_symmetries(H, len(H.wires))
opt_sector = qml.hf.optimal_sector(H, generators, mol.n_electrons)
# taper the Hamiltonian
H_tapered = qml.hf.transform_hamiltonian(H, generators, paulix_ops, opt_sector)
We can compare the number of qubits required by the original Hamiltonian and the tapered Hamiltonian:
>>> len(H.wires)
4
>>> len(H_tapered.wires)
2
For quantum chemistry algorithms, the HartreeFock state can also be tapered:
n_elec = mol.n_electrons
n_qubits = mol.n_orbitals * 2
hf_tapered = qml.hf.transform_hf(
generators, paulix_ops, opt_sector, n_elec, n_qubits
)
Qiskit Runtime support 🏃♂️
With version 0.21, the PennyLaneQiskit plugin now provides initial Qiskit Runtime support.
Previously, when using PennyLane with IBM quantum hardware via the PennyLaneQiskit plugin, each circuit evaluation would be separately sent and queued for execution on hardware, which can slow down classical optimization loops with many iterations.
In the last release, we added support for batch execution of circuits, massively improving the time taken to submit multiple, independent circuit (for example, when computing quantum gradients).
With our experimental Qiskit Runtime support, more of the computation is moved serverside, further reducing overhead.
Qiskit Runtime support in PennyLane takes two forms: runtime devices and workflow runners.
Runtime devices
Two Qiskit Runtime devices are available for circuit sampling, circuitrunner
and sampler
.
Simply instantiate the following devices, specify a backend, and provide any additional options:
dev = qml.device('qiskit.ibmq.circuit_runner', wires=2, backend='ibmq_qasm_simulator', shots=8000, **kwargs)
dev = qml.device('qiskit.ibmq.sampler', wires=2, backend='ibmq_qasm_simulator', shots=8000, **kwargs)
Workflow runners
Not all Qiskit Runtime programs correspond to complete devices, some solve specific problems such as VQE.
A custom Qiskit VQE program has been added and can be used via the vqe_runner
function. Simply upload this
Runtime program at the beginning of your session, retrieve the program ID, and use it within the VQE runner.
from pennylane_qiskit import upload_vqe_runner, vqe_runner
program_id = upload_vqe_runner(hub="ibmq", group="open", project="main")
def vqe_circuit(params):
qml.RX(params[0], wires=0)
qml.RY(params[1], wires=0)
job = vqe_runner(
program_id=program_id,
backend="ibmq_qasm_simulator",
hamiltonian=1.0 * qml.PauliX(0) + 1.0 * qml.PauliZ(0),
ansatz=vqe_circuit,
x0=[3.97507603, 3.00854038],
shots=8000,
optimizer="SPSA",
optimizer_config={"maxiter": 40},
kwargs={"hub": "ibmq", "group": "open", "project": "main"},
)
For more details on Qiskit Runtime support, please see the PennyLaneQiskit plugin . Note that Qiskit Runtime support is currently experimental — if you come across any issues, please let us know with a GitHub issue.
Improved quantumaware optimizers 📉
Quantumaware optimizers generally provide the best of both worlds here; taking into account the geometry of the quantum landscape to improve convergence while reducing quantum resources.
In this release we have significant performance and capability improvements across our suite of quantumaware optimizers.
Rotosolve optimization with arbitrary circuits and processing
The Rotosolve optimizer — a quantumaware optimization method which performs coordinate minimization within the quantum cost landscape — previously only supported variational circuits with a subset of supported gates, and no internal classical processing.
Now, circuits with arbitrary gates and linear classical processing are supported natively without decomposition, as long as the frequencies of the gate parameters are known. This new generalization extends the Rotosolve optimization method to a larger class of algorithms, and can reduce the cost of the optimization compared to decomposing all gates to singlequbit rotations.
Consider the following QNode, containing a mixture of singlequbit Pauli rotations, and more complicated parametrized gates such as a controlled PauliY rotation.
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def qnode(x, y):
qml.RX(2.5 * x, wires=0)
qml.CNOT(wires=[0, 1])
qml.RZ(0.3 * y[0], wires=0)
qml.CRY(1.1 * y[1], wires=[1, 0])
return qml.expval(qml.PauliX(0) @ qml.PauliZ(1))
x = np.array(0.8, requires_grad=True)
y = np.array([0.2, 1.5], requires_grad=True)
Its frequency spectra can be easily obtained via
qml.fourier.qnode_spectrum
:
>>> spectra = qml.fourier.qnode_spectrum(qnode)(x, y)
>>> spectra
{'x': {(): [2.5, 0.0, 2.5]},
'y': {(0,): [0.3, 0.0, 0.3], (1,): [1.1, 0.55, 0.0, 0.55, 1.1]}}
We can then use this information to use Rotosolve to optimize this circuit:
>>> print("Initial cost:", np.round(qnode(x, y), 3))
Initial cost: 0.706
>>> opt = qml.RotosolveOptimizer()
>>> for _ in range(2):
... x, y = opt.step(qnode, x, y, spectra=spectra)
... print(f"New cost: {np.round(qnode(x, y), 3)}")
New cost: 0.0
New cost: 1.0
For more details, see the qml.RotosolveOptimizer
documentation.
Speedier quantum natural gradients
A new function for computing the metric tensor on simulators,
qml.adjoint_metric_tensor
,
has been added, that uses classically efficient methods to massively improve performance.
This new functionality can be used with
qml.QNGOptimizer
to speed up quantum natural gradientbased optimization.
Compare the performance against the hardwarecompatible
qml.metric_tensor
on a 6 wire simulator:
>>> dev = qml.device("default.qubit", wires=4, shots=None)
>>> @qml.qnode(dev)
... def circuit(weights):
... qml.StronglyEntanglingLayers(weights, wires=range(3))
... return qml.expval(qml.PauliZ(0) @ qml.PauliZ(2))
>>> weights = np.random.random([4, 3, 3], requires_grad=True)
>>> %timeit qml.metric_tensor(circuit)(weights)
8.13 s ± 966 ms per loop
>>> %timeit qml.adjoint_metric_tensor(circuit)(weights)
5.05 s ± 327 ms per loop
For more details, see the qml.adjoint_metric_tensor
documentation.
Better JAX support 🤖
If you’ve been using PennyLane + JAX + quantum hardware, the integration gets a whole lot better in this release with the support of vectorvalued QNodes.
Previously, when using JAX with hardware or the parametershift rule,
only QNodes that returned single expectation values were supported. In v0.21, you can
now return multiple quantum measurement statistics, as well as vectorvalued
measurements such as qml.probs
:
dev = qml.device('default.qubit', wires=2)
@qml.qnode(dev, diff_method="parametershift", interface="jax")
def circuit(x):
qml.RX(x[0], wires=[0])
qml.RY(x[1], wires=[1])
qml.CNOT(wires=[0, 1])
return qml.probs(wires=[1])
>>> x = jnp.array([0.543, 0.654])
>>> circuit(x, y)
DeviceArray([0.8397495 , 0.16025047], dtype=float32)
>>> jax.jacobian(circuit, argnums=[0, 1])(x, y)
DeviceArray([[0.2050439, 0.26043 ],
[ 0.2050439, 0.26043 ]], dtype=float32)
Note that jax.jit
is not yet supported for vectorvalued QNodes.
New templates and transforms 🏗️
Alongside the great new features above, we also have new (differentiable!) templates and transforms to share.
Two new tensornetwork templates
Tensornetwork templates create quantum circuit architectures where circuit blocks can be broadcast with the shape and connectivity of tensor networks. Two new tensornetwork templates in this release includes:
qml.MPS
for initializing matrix product state tensor networks
qml.TTN
for creating tree tensor networks.
Hardwarecompatible Hessian transform
A new quantum gradient transform,
qml.gradients.param_shift_hessian
,
allows direct computation of second order derivatives on hardware, while minimizing the number of
circuit executions.
>>> dev = qml.device("default.qubit", wires=2)
>>> @qml.qnode(dev)
... def circuit(x):
... qml.RX(x[0], wires=0)
... qml.RY(x[1], wires=0)
... return qml.expval(qml.PauliZ(0))
>>> x = np.array([0.1, 0.2], requires_grad=True)
>>> qml.gradients.param_shift_hessian(circuit)(x)
tensor([[0.97517033, 0.01983384],
[ 0.01983384, 0.97517033]], requires_grad=True)
Improvements
In addition to the new features listed above, the release contains a wide array of improvements and optimizations:

Performance improvements for
lightning.qubit
: new highlyperformant C++ kernels for quantum gates have been added tolightning.qubit
. For more details, please see thelightning.qubit
release notes. The new kernels significantly improve the runtime performance of PennyLaneLightning for both differentiable and nondifferentiable workflows:
 PennyLane and
lightning.qubit
now support Python 3.10.
 The
qml.transforms.insert
transform now supports inserting operations after or before specified gates in the circuit.

A more efficient method of Hamiltonian simplification has been added to the
hf
module asqml.hf.simplify
.This function combines redundant terms in a Hamiltonian and eliminates terms with a coefficient smaller than a cutoff value, and will eventually replace the logic in
Hamiltonian.simplify()
. For example, the time to construct the Hamiltonian of LiH is reduced roughly by a factor of 20.
 The QAOA module now accepts both NetworkX and RetworkX graphs as function inputs.
 The
CircuitGraph
, used to represent circuits via directed acyclic graphs, now uses RetworkX for its internal representation. This results in significant speedup for algorithms that rely on a directed acyclic graph representation.
 For subclasses of
Operator
where the number of parameters is known before instantiation, thenum_params
is reverted back to being a static property. This allows to programmatically know the number of parameters before an operator is instantiated without changing the user interface.
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 changing in this release.
Defining trainable parameters in Autograd
QNode arguments will no longer be considered trainable by default when using the Autograd interface.
In order to obtain derivatives with respect to a parameter, trainable parameters should be
instantiated via PennyLane’s NumPy wrapper using the requires_grad=True
attribute.
x = np.array([0.1, 0.2], requires_grad=True)
qml.grad(qnode)(x)
Alternatively, trainability can be indicated via the argnum
keyword argument passed to qml.grad
/qml.jacobian
:
x = np.array([0.1, 0.2])
qml.grad(circuit, argnum=1)(0.5, x)
Jacobian output shapes when using Autograd
qml.jacobian
now follows a different convention regarding its output shape.
Previously, qml.jacobian
would attempt to stack the Jacobian for multiple
QNode arguments, which succeeded whenever the arguments have the same shape:
>>> @qml.qnode(qml.device("default.qubit", wires=1))
... def circuit(x, y):
... qml.RX(x, wires=0)
... qml.RY(y, wires=0)
... return qml.probs(wires=0)
>>> x = np.array(0.2, requires_grad=True)
>>> y = np.array(0.6, requires_grad=True)
>>> qml.jacobian(circuit)(x, y)
array([[0.08198444, 0.27669361],
[ 0.08198444, 0.27669361]])
With this release, for QNodes with multuple arguments, the output shape instead is a tuple, where
each entry corresponds to one QNode argument and has the shape (*output_shape, *argument_shape)
.
>>> qml.jacobian(circuit)(x, y)
(array([0.08198444, 0.08198444]), array([0.27669361, 0.27669361]))
This ensures that the output shape of qml.jacobian
is consistent, inline with the behaviour of
other autodifferentiation frameworks, and does not depend on the QNode return value.
Note that the behaviour of qml.jacobian
is unchanged if the QNode only accepts
one trainable parameter, or if the argnum
argument is provided.
Rotosolve arguments
The keyword arguments for the qml.RotosolveOptimizer
have been modified; please refer to the
latest
docstring
for more information.
These highlights are just scratching the surface — check out the full release notes for more details.
Contributors
As always, this release would not have been possible without the hard work of our development team and contributors:
Guillermo AlonsoLinaje, Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Sam Banning, Thomas Bromley, Esther Cruz, Amintor Dusko, Christian Gogolin, Nathan Killoran, Christina Lee, Olivia Di Matteo, Diego Guala, Anthony Hayes, David Ittah, Josh Izaac, Soran Jahangiri, Edward Jiang, Ankit Khandelwal, Korbinian Kottmann, Romain Moyard, Lee James O’Riordan, ChaeYeun Park, Tanner Rogalsky, Maria Schuld, Jay Soni, Antal Száva, David Wierichs, Shaoming Zhang.