31 is kind of a special number — it belongs to the smallest non-palindromic emirp pair: 13, 31 🤓. That, and it's a pretty sweet version of PennyLane! PennyLane v0.31 is out today! Check out all of the awesome new functionality below.
Contents
- Seamlessly create and combine fermionic operators 🔬
- GPU-distributed statevector support 🤖🤖
- Workflow-level quantum circuit resource estimation 🧮
- Community contributions from unitaryHACK 🤝
- Improvements 🛠
- Deprecations and breaking changes 💔
- Contributors ✍️
Seamlessly create and combine fermionic operators 🔬
We stand fermily behind this new feature 🤠. Fermionic operators and arithmetic are now available in PennyLane!

With these new features, you can create fermionic operator Hamiltonians in a couple of different ways:
-
qml.FermiC
andqml.FermiA
: the fermionic creation and annihilation operators, respectively. These operators are defined by passing the index of the orbital that the fermionic operator acts on. For instance, the operators a^{\dagger}_0 and a_3 are respectively constructed as>>> qml.FermiC(0) a⁺(0) >>> qml.FermiA(3) a(3)
These operators can be linearly combined (
+
and-
) with, and multiplied (*
) by other Fermi operators to create arbitrary fermionic Hamiltonians. Multiplying several Fermi operators together creates an operator that we call a Fermi word:>>> word = qml.FermiC(0) * qml.FermiA(0) * qml.FermiC(3) * qml.FermiA(3) >>> word a⁺(0) a(0) a⁺(3) a(3)
Fermi words can be linearly combined to create a fermionic operator that we call a Fermi sentence:
>>> sentence = 1.2 * word + 0.345 * qml.FermiC(3) * qml.FermiA(3) >>> sentence 1.2 * a⁺(0) a(0) a⁺(3) a(3) + 0.345 * a⁺(3) a(3)
-
qml.fermi.from_string
: create a fermionic operator that represents multiple creation and annihilation operators being multiplied by each other (a Fermi word).>>> qml.fermi.from_string('0+ 1- 0+ 1-') a⁺(0) a(1) a⁺(0) a(1) >>> qml.fermi.from_string('0^ 1 0^ 1') a⁺(0) a(1) a⁺(0) a(1)
Fermi words created with
from_string
can be linearly combined to create a Fermi sentence:>>> word1 = qml.fermi.from_string('0+ 0- 3+ 3-') >>> word2 = qml.fermi.from_string('3+ 3-') >>> sentence = 1.2 * word1 + 0.345 * word2 >>> sentence 1.2 * a⁺(0) a(0) a⁺(3) a(3) + 0.345 * a⁺(3) a(3)
Fermi words and sentences can also be created via
qml.fermi.FermiWord
and
qml.fermi.FermiSentence
,
respectively.
Additionally, any fermionic operator, be it a single fermionic creation/annihilation operator, a Fermi word, or a Fermi sentence,
can be mapped to the qubit basis by using
qml.jordan_wigner
:
>>> qml.jordan_wigner(sentence) ((0.4725+0j)*(Identity(wires=[0]))) + ((-0.4725+0j)*(PauliZ(wires=[3]))) + ((-0.3+0j)*(PauliZ(wires=[0]))) + ((0.3+0j)*(PauliZ(wires=[0]) @ PauliZ(wires=[3])))
Learn how to create fermionic Hamiltonians describing some simple chemical systems by checking out our fermionic operators demo!
GPU-distributed statevector support 🤖🤖
You know what's better than using one GPU? Using multiple GPUs 😀. PennyLane-Lightning-GPU now supports multi-node/multi-GPU calculations!

Use of PennyLane-Lightning-GPU with multi-node/multi-GPU support requires
explicit installation of the NVIDIA cuQuantum SDK
(current supported cuQuantum
version: cuquantum-cu11
), mpi4py
and CUDA-aware MPI
(Message Passing Interface). Check out the instructions in
the
PennyLane-Lightning-GPU documentation
for more details.
With that out of the way, let's go through an example calculation. We need to
define a way for the parallel GPUs to communicate with each other. The
protocol for doing so is the Message Passing Interface (MPI) — taken care of
with the mpi4py
package. Let's define a script called mpi_gpu.py
:
from mpi4py import MPI import pennylane as qml from pennylane import numpy as np
The object that represents the processes that communicate with each other is
called a communicator (comm
in the example below):
comm = MPI.COMM_WORLD rank = comm.Get_rank()
To demonstrate the power of this new feature, let's simulate 31 qubits. The
MPI
backend will be called if mpi=True
when defining a device.
n_wires = 31 n_layers = 1 dev = qml.device('lightning.gpu', wires= n_wires, mpi=True) @qml.qnode(dev, diff_method="adjoint") def circuit_adj(weights): qml.StronglyEntanglingLayers(weights, wires=list(range(n_wires))) return qml.expval(qml.PauliZ(0)) if rank == 0: params = np.random.random(qml.StronglyEntanglingLayers.shape(n_layers=n_layers, n_wires=n_wires)) else: params = None
Lastly, we need to ensure that every GPU being used receives the same
parameters with bcast
:
params = comm.bcast(params, root=0) jac = qml.jacobian(circuit_adj)(params) if rank==0: print(jac)
[[[ 1.74049378e-17 -1.24668737e-19 1.75018936e-17] ... [ 9.78915458e-19 -3.75031201e-03 -1.52691202e-18]]]
With our circuit defined (and in this case 4 GPUs available) we can run the above circuit with:
mpirun -np 4 python mpi_gpu.py
We hope you enjoy this truly awesome feature!
Workflow-level quantum circuit resource estimation 🧮
Work smarter 🧠, not harder, by utilizing a new set of quantum circuit resource estimation functionalities!

PennyLane's Tracker
PennyLane's
Tracker
now monitors the resource requirements of circuits being executed by the device.
Suppose we have a workflow that involves executing circuits with different
qubit numbers. We can obtain the resource requirements as a function of the
number of qubits by executing the workflow with the Tracker
context:
dev = qml.device("default.qubit", wires=4) @qml.qnode(dev) def circuit(n_wires): for i in range(n_wires): qml.Hadamard(i) return qml.probs(range(n_wires)) with qml.Tracker(dev) as tracker: for i in range(1, 5): circuit(i)
The resource requirements of individual circuits can then be inspected as follows:
>>> resources = tracker.history["resources"] >>> resources[0] wires: 1 gates: 1 depth: 1 shots: Shots(total=None) gate_types: {'Hadamard': 1} gate_sizes: {1: 1} >>> [r.num_wires for r in resources] [1, 2, 3, 4]
Moreover, it is possible to predict the resource requirements without
evaluating circuits using the null.qubit
device, which follows the standard
execution pipeline but returns numeric zeros. Check out the full
v0.31 release notes
to learn more!
Custom operations
Custom operations can now be defined that solely include resource requirements
— an explicit decomposition or matrix representation is not needed, allowing
you to estimate requirements for high-level algorithms composed of abstract
subroutines.
These operations can be defined by inheriting from
ResourcesOperation
and overriding the resources()
method to return an appropriate
Resources
object:
class CustomOp(qml.resource.ResourcesOperation): def resources(self): n = len(self.wires) r = qml.resource.Resources( num_wires=n, num_gates=n ** 2, depth=5, ) return r
>>> wires = [0, 1, 2] >>> c = CustomOp(wires) >>> c.resources() wires: 3 gates: 9 depth: 5 shots: Shots(total=None) gate_types: {} gate_sizes: {}
A quantum circuit that contains CustomOp
can be created and inspected using
qml.specs
:
dev = qml.device("default.qubit", wires=wires) @qml.qnode(dev) def circ(): qml.PauliZ(wires=0) CustomOp(wires) return qml.state()
>>> specs = qml.specs(circ)() >>> specs["resources"].depth 6
Community contributions from unitaryHACK 🤝
This year's unitaryHACK proved that PennyLane's community is as strong as ever 💪! Here's a glimpse of what the community contributed.

Trace distance
The quantum information module now supports trace distance in a couple of ways:
-
A QNode transform via
qml.qinfo.trace_distance
:dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit(param): qml.RY(param, wires=0) qml.CNOT(wires=[0, 1]) return qml.state()
>>> trace_distance_circuit = qml.qinfo.trace_distance(circuit, circuit, wires0=[0], wires1=[0]) >>> x, y = np.array(0.4), np.array(0.6) >>> trace_distance_circuit((x,), (y,)) 0.047862689546603415
-
Flexible post-processing via
qml.math.trace_distance
:>>> rho = np.array([[0.3, 0], [0, 0.7]]) >>> sigma = np.array([[0.5, 0], [0, 0.5]]) >>> qml.math.trace_distance(rho, sigma) 0.19999999999999998
Qutrit basis state preparation
It is now possible to prepare qutrit basis states with qml.QutritBasisState
.
wires = range(2) dev = qml.device("default.qutrit", wires=wires) @qml.qnode(dev) def qutrit_circuit(): qml.QutritBasisState([1, 1], wires=wires) qml.TAdd(wires=wires) return qml.probs(wires=1)
>>> qutrit_circuit() array([0., 0., 1.])
A unified decomposition transform
A new transform called
one_qubit_decomposition
has been added that provides a unified interface for decompositions of a
single-qubit unitary matrix into sequences of X, Y, and Z rotations. All
decompositions simplify the rotations angles to be between 0
and 4π
.
>>> from pennylane.transforms import one_qubit_decomposition >>> U = np.array([[-0.28829348-0.78829734j, 0.30364367+0.45085995j], ... [ 0.53396245-0.10177564j, 0.76279558-0.35024096j]]) >>> one_qubit_decomposition(U, 0, "ZYZ") [RZ(tensor(12.32427531, requires_grad=True), wires=[0]), RY(tensor(1.14938178, requires_grad=True), wires=[0]), RZ(tensor(1.73305815, requires_grad=True), wires=[0])] >>> one_qubit_decomposition(U, 0, "XYX", return_global_phase=True) [RX(tensor(10.84535137, requires_grad=True), wires=[0]), RY(tensor(1.39749741, requires_grad=True), wires=[0]), RX(tensor(0.45246584, requires_grad=True), wires=[0]), (0.38469215914523336-0.9230449299422961j)*(Identity(wires=[0]))]
For a complete list of contributions, check out the full release notes.
Improvements 🛠
In addition to the new features listed above, the release contains a wide array of improvements and optimizations:
-
The
TorchLayer
andKerasLayer
integrations withtorch.nn
andKeras
have been upgraded with the following new features:-
Native support for parameter broadcasting.
n_qubits = 2 dev = qml.device("default.qubit", wires=n_qubits) @qml.qnode(dev) def qnode(inputs, weights): qml.AngleEmbedding(inputs, wires=range(n_qubits)) qml.BasicEntanglerLayers(weights, wires=range(n_qubits)) return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_qubits)] n_layers = 6 weight_shapes = {"weights": (n_layers, n_qubits)} qlayer = qml.qnn.TorchLayer(qnode, weight_shapes)
>>> batch_size = 10 >>> inputs = torch.rand((batch_size, n_qubits)) >>> qlayer(inputs) >>> dev.num_executions == 1 True
-
The ability to draw a
TorchLayer
andKerasLayer
usingqml.draw()
andqml.draw_mpl()
. -
Support for
KerasLayer
model saving and clearer instructions onTorchLayer
model saving (this includes loading/saving hybrid models).
-
-
The stochastic parameter-shift gradient method can now be used with hardware-compatible Hamiltonians. This new feature generalizes
stoch_pulse_grad
to support Hermitian generating terms beyond just Pauli words in pulse Hamiltonians, which makes it hardware-compatible. -
A new differentiation method called
qml.gradients.pulse_generator
is available, which combines classical processing with the parameter-shift rule for multivariate gates to differentiate pulse programs. Access it in your pulse programs by settingdiff_method=qml.gradients.pulse_generator
. -
Reduced density matrix functionality has been added via
qml.math.reduce_dm
andqml.math.reduce_statevector
. Both functions have broadcasting support. -
A whole host of existing operators, transforms, and more now support parameter broadcasting:
- Qutrit devices
- The following functions in
qml.qinfo
:purity
,vn_entropy
,mutual_info
,fidelity
,relative_entropy
,trace_distance
- The following functions in
qml.math
:purity
,vn_entropy
,mutual_info
,fidelity
,relative_entropy
,max_entropy
,sqrt_matrix
Deprecations and breaking changes 💔
As new things are added, outdated features are removed. To keep track of things in the deprecation pipeline, check out the deprecations page.
Here's a summary of what has changed in this release:
-
qml.collections
,qml.op_sum
, andqml.utils.sparse_hamiltonian
have been removed. -
qml.math.reduced_dm
has been deprecated. Please useqml.math.reduce_dm
orqml.math.reduce_statevector
instead. -
zyz_decomposition
andxyx_decomposition
are now deprecated in favour ofone_qubit_decomposition
. -
qml.math.purity
,qml.math.vn_entropy
,qml.math.mutual_info
,qml.math.fidelity
,qml.math.relative_entropy
, andqml.math.max_entropy
no longer support state vectors as input. Please callqml.math.dm_from_state_vector
on the input before passing to any of these functions.
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:
Venkatakrishnan AnushKrishna, Thomas Bromley, Isaac De Vlugt, Amintor Dusko, Tarik El-Khateeb, Lillian M. A. Frederiksen, Emiliano Godinez Ramirez, Nikhil Harle, Soran Jahangiri, Edward Jiang, Korbinian Kottmann, Ivana Kurečić, Christina Lee, Vincent Michaud-Rioux, Romain Moyard, Tristan Nemoz, Lee James O'Riordan, Mudit Pandey, Chae-Yeun Park, Manul Patel, Borja Requena, Modjtaba Shokrian-Zini, Mainak Roy, Shuli Shu, Matthew Silverman, Jay Soni, Edward Thomas, David Wierichs, and Frederik Wilde.
About the authors
Isaac De Vlugt
My job is to help manage the PennyLane and Catalyst feature roadmap... and spam lots of emojis in the chat 🤠