We hope you’ve recovered from the excitement of QHack 2023, because here’s something more for you — the release of PennyLane 0.29! Check out all of the awesome new functionality below.
Feel the pulse 🔊
You might be familiar with constructing quantum circuits using gates like singlequbit rotations and CNOTs as their building blocks. But there is another way to do this! Quantum hardware often implements the gates we are familiar with using a sequence of carefully calibrated laser pulses. This release of PennyLane allows you to control those pulses directly, unlocking a new toolset to construct, simulate, and differentiate pulsebased quantum circuits 🔊.
Pulses and timedependent Hamiltonians
A pulse can be thought of as a timedependent function that sets a coefficient in a Hamiltonian:
>>> import jax
>>> from jax import numpy as jnp
>>> pulse = lambda p, t: p[0] * jnp.sin(p[1] * t + p[2])
>>> H = qml.PauliZ(0) + pulse * qml.PauliX(0)
>>> p = jnp.array([[0.9, 1.1, 0.1]])
>>> t = 1.0
>>> H(p, t)
(1*(PauliZ(wires=[0]))) + (0.8388351798057556*(PauliX(wires=[0])))
This timedependent Hamiltonian, H
, determines the evolution of a singlequbit system with time.
Creating pulsebased circuits
We can use H
to create a pulsebased circuit using
qml.evolve
:
dev = qml.device("default.qubit.jax", wires=1)
@jax.jit
@qml.qnode(dev, interface="jax")
def circuit(p, t):
qml.evolve(H)(p, t=t)
return qml.expval(qml.PauliZ(0))
Pulsebased circuits can be executed using the jax
interface:
>>> t = jnp.array([0, 5])
>>> circuit(p, t)
Array(0.86624, dtype=float32)
Moreover, we can also differentiate the circuit with respect to its parameters, p
:
>>> jax.grad(circuit)(p, t)
Array([[0.7991514 , 1.7720929 , 0.06342924]], dtype=float32)
Stay tuned! We’ll be releasing a demo in a few weeks’ time to show what you can do with pulselevel programming!
Here comes the SU(N) 🌞
Tired of working out which gates to put where in your circuit? Would you like something a bit more
flexible? With the new
qml.SpecialUnitary
gate — which realizes an SU(N) transformation — you can apply an
arbitrary unitary to a collection of qubits. What’s more, you can optimize the parameters of
qml.SpecialUnitary
in a hardwarecompatible way, allowing you to create expressive circuits without
worrying about your choice of gates!
Creating an \(n\)qubit unitary
We can generate \(n\)qubit unitaries from the SU(N) group, where \(N=2^n\). To do this, we need to choose a vector \(\vec{\theta}\) of length \(d = 4^n  1\). This vector sets the angles corresponding to the \(n\)qubit Pauli words:
>>> qml.ops.qubit.special_unitary.pauli_basis_strings(1) # 4**11 = 3 Pauli words
['X', 'Y', 'Z']
>>> qml.ops.qubit.special_unitary.pauli_basis_strings(2) # 4**21 = 15 Pauli words
['IX', 'IY', 'IZ', 'XI', 'XX', 'XY', 'XZ', 'YI', 'YX', 'YY', 'YZ', 'ZI', 'ZX', 'ZY', 'ZZ']
For example, on a single qubit, we may define
>>> from jax import numpy as jnp
>>> import jax
>>> theta = jnp.array([0.2, 0.1, 0.5])
>>> U = qml.SpecialUnitary(theta, 0)
>>> U.matrix()
Array([[ 0.8537127 0.47537234j, 0.09507447+0.19014893j],
[0.09507447+0.19014895j, 0.8537127 +0.47537234j]], dtype=complex64)
The unitary corresponding to \(\vec{\theta}\) is given by \(U(\vec{\theta}) = \exp \left(\sum_{m=1}^{d} \theta_{m} P_{m} \right)\), where \(P_{m}\) are Pauli words.
Executing and differentiating SU(N)
The
qml.SpecialUnitary
operation can be included inside a PennyLane circuit:
dev = qml.device("default.qubit", wires=1)
@qml.qnode(dev, interface="jax", diff_method="parametershift")
def circuit(theta):
qml.SpecialUnitary(theta, wires=0)
return qml.expval(qml.PauliZ(0))
This circuit can be executed and differentiated:
>>> circuit(theta)
Array(0.9096085, dtype=float32)
>>> jax.grad(circuit)(theta)
Array([0.710832 , 0.355416 , 0.03075087], dtype=float32)
Note that we are using the hardwarecompatible "parametershift"
method for gradient calculations!
Check out the qml.SpecialUnitary
documentation to
understand how this is working!
Always differentiable 📈
You might have guessed that, here at PennyLane, we love calculating derivatives! Well, this release is no exception; we’ve added lots of new tools to help make your autodiff life easier 😎. We’re particularly excited about the addition of two new gradient methods: the Hadamard test and SPSA.
The Hadamard test
The Hadamard test is a hardwarecompatible method that allows you to calculate gradients with fewer circuit executions, at the cost of an additional auxiliary qubit.
>>> with qml.tape.QuantumTape() as tape:
... qml.RX(0.1, wires=0)
... qml.RY(0.2, wires=0)
... qml.RX(0.3, wires=0)
... qml.expval(qml.PauliZ(0))
>>> print(tape.draw(decimals=2))
0: ──RX(0.10)──RY(0.20)──RX(0.30)─┤ <Z>
>>> qml.enable_return()
>>> gradient_tapes, fn = qml.gradients.hadamard_grad(tape)
>>> print(gradient_tapes[0].draw(decimals=2))
0: ──RX(0.10)─╭X──RY(0.20)──RX(0.30)─┤ ╭<Z@Y>
1: ──H────────╰●──H──────────────────┤ ╰<Z@Y>
If you’re not familiar with our lowlevel
qml.tape.QuantumTape
circuit representation, don’t worry, the
example above is just for illustration. If you want to use the Hadamard test method to calculate
gradients, simply request hadamard
when selecting the diff_method
in your
QNode
:
>>> import torch
>>> qml.enable_return()
>>> dev = qml.device("default.qubit", wires=2)
>>> @qml.qnode(dev, interface="torch", diff_method="hadamard")
>>> def circuit(params):
... qml.RX(params[0], wires=0)
... qml.RY(params[1], wires=0)
... qml.RX(params[2], wires=0)
... return qml.expval(qml.PauliZ(0))
>>> params = torch.tensor([0.1, 0.2, 0.3], requires_grad=True)
>>> res = circuit(params)
>>> res.backward()
>>> params.grad
tensor([0.3875, 0.1888, 0.3836])
SPSA
This release also allows you to request the SPSA gradient method directly within a QNode.
Here is an example using the torch
interface:
>>> import torch
>>> qml.enable_return()
>>> dev = qml.device("default.qubit", wires=2)
>>> @qml.qnode(dev, interface="torch", diff_method="spsa", h=0.05, num_directions=20)
>>> def circuit(params):
... qml.RX(params[0], wires=0)
... qml.RY(params[1], wires=0)
... qml.RX(params[2], wires=0)
... return qml.expval(qml.PauliZ(0))
>>> params = torch.tensor([0.1, 0.2, 0.3], requires_grad=True)
>>> res = circuit(params)
>>> res.backward()
>>> params.grad
tensor([0.1772, 0.1105, 0.2467])
The returned value is an estimator for the true gradient. Check out the documentation for more details!
Smartly decompose Hamiltonian evolution 💯
For those of you who love to timeevolve Hamiltonians, things just got even better! Now you can break down that evolution into more elementary operations.
If the timeevolved Hamiltonian is equivalent to another PennyLane operation, then that operation is returned as the decomposition:
>>> exp_op = qml.evolve(qml.PauliX(0) @ qml.PauliX(1))
>>> exp_op.decomposition()
[IsingXX((2+0j), wires=[0, 1])]
If the Hamiltonian is a Pauli word, then the decomposition is provided as a
qml.PauliRot
operation:
>>> qml.evolve(qml.PauliZ(0) @ qml.PauliX(1)).decomposition()
[PauliRot((2+0j), ZX, wires=[0, 1])]
Otherwise, the Hamiltonian is a linear combination of operators and the Suzuki–Trotter decomposition is used:
>>> sum = qml.sum(qml.PauliX(0), qml.PauliY(0), qml.PauliZ(0))
>>> qml.evolve(sum, num_steps=2).decomposition()
[RX((1+0j), wires=[0]),
RY((1+0j), wires=[0]),
RZ((1+0j), wires=[0]),
RX((1+0j), wires=[0]),
RY((1+0j), wires=[0]),
RZ((1+0j), wires=[0])]
This decomposition is an approximation. Increasing num_steps
will result in a closer
approximation to your target evolution, at a cost of increased circuit depth.
Improvements 🛠
In addition to the new features listed above, the release contains a wide array of improvements and optimizations:
 The default interface is now
auto
. There is no need to specify the interface anymore; it is automatically determined by checking your QNode parameters:
import jax
import jax.numpy as jnp
qml.enable_return()
a = jnp.array(0.1)
b = jnp.array(0.2)
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def circuit(a, b):
qml.RY(a, wires=0)
qml.RX(b, wires=1)
qml.CNOT(wires=[0, 1])
return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1))
>>> circuit(a, b)
(Array(0.9950042, dtype=float32), Array(0.19767681, dtype=float32))
>>> jac = jax.jacobian(circuit)(a, b)
>>> jac
(Array(0.09983341, dtype=float32, weak_type=True),
Array(0.01983384, dtype=float32, weak_type=True))
 The function called
qml.dot
has been updated to compute the dot product between a vector and a list of operators:
>>> coeffs = np.array([1.1, 2.2])
>>> ops = [qml.PauliX(0), qml.PauliY(0)]
>>> qml.dot(coeffs, ops)
(1.1*(PauliX(wires=[0]))) + (2.2*(PauliY(wires=[0])))
>>> qml.dot(coeffs, ops, pauli=True)
1.1 * X(0) + 2.2 * Y(0)
 The
default.mixed
device has received a performance improvement for multiqubit operations. This also allows you to apply channels that act on more than seven qubits, which was not possible before.
qml.draw
andqml.draw_mpl
have been updated to draw any quantum function, which allows for visualizing only part of a complete circuit/QNode.
qml.qchem.basis_rotation
now accounts for spin, allowing it to perform basis rotation groupings for molecular Hamiltonians.

The
qml.math
module now also contains a submodule for fast Fourier transforms,qml.math.fft
.The submodule in particular provides differentiable versions of the following functions, available in all common interfaces for PennyLane: fft, ifft, fft2, and ifft2.
Note that the output of the derivatives of these functions may differ when used with complexvalued inputs, due to different conventions on complexvalued derivatives.
 Most quantum channels are now fully differentiable on all interfaces.
 Writing Hamiltonians to a file using the
qml.data
module has been improved by employing a condensed writing format.
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:
 When a QNode interface is not specified, it is determined during the QNode call instead of the
initialization. This means that the
gradient_fn
andgradient_kwargs
are only defined on the QNode at the beginning of the call. Furthermore, without specifying the interface it is not possible to guarantee that the device will not be changed during the call if you are using backprop (for example, the device may change fromdefault.qubit
todefault.qubit.jax
). If you would like to interact with the device after calling a QNode, you should specify the interface you want to use.
Operation.inv()
and theOperation.inverse
setter have been removed. Please useqml.adjoint
orqml.pow
instead.
op.simplify()
for operators which are linear combinations of Pauli words will use a builtin Pauli representation to more efficiently compute the simplification of the operator.
 The
collections
module has been deprecated.
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:
GianLuca Anselmetti, Guillermo AlonsoLinaje, Juan Miguel Arrazola, Ikko Ashimine, Utkarsh Azad, Miriam Beddig, Cristian Boghiu, Thomas Bromley, Astral Cai, Isaac De Vlugt, Olivia Di Matteo, Amintor Dusko, Lillian M. A. Frederiksen, Soran Jahangiri, Korbinian Kottmann, Christina Lee, Vincent MichaudRioux, Albert Mitjans Coma, Romain Moyard, Lee J. O’Riordan, Mudit Pandey, ChaeYeun Park, Borja Requena, Shuli Shu, Matthew Silverman, Jay Soni, Antal Száva, Frederik Wilde, David Wierichs, Moritz Willmann.