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.
Contents
- Feel the pulse π
- Here comes the SU(N) π
- Always differentiable π
- Smartly decompose Hamiltonian evolution π―
- Improvements π
- Deprecations and breaking changes π
- Contributors βοΈ
Feel the pulse π

You might be familiar with constructing quantum circuits using gates like single-qubit 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 pulse-based quantum circuits π.
Pulses and time-dependent Hamiltonians
A pulse can be thought of as a time-dependent 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 time-dependent Hamiltonian, H
, determines the evolution of a single-qubit system with time.
Creating pulse-based circuits
We can use H
to create a pulse-based 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))
Pulse-based 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 pulse-level 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 hardware-compatible 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**1-1 = 3 Pauli words ['X', 'Y', 'Z'] >>> qml.ops.qubit.special_unitary.pauli_basis_strings(2) # 4**2-1 = 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="parameter-shift") 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 hardware-compatible "parameter-shift"
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 hardware-compatible 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 low-level
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 time-evolve Hamiltonians, things just got even better! Now you can break down that evolution into more elementary operations.
If the time-evolved 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 multi-qubit 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 complex-valued inputs, due to different conventions on complex-valued 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 built-in 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:
Gian-Luca Anselmetti, Guillermo Alonso-Linaje, Juan Miguel Arrazola, Ikko Ashimine, Utkarsh Azad, Miriam Beddig, Cristian Boghiu, Thomas Bromley, Astral Cai, Isaac De Vlugt, Olivia Di Matteo, Amintor Dusko, Tarik El-Khateeb, Lillian M. A. Frederiksen, Soran Jahangiri, Korbinian Kottmann, Christina Lee, Vincent Michaud-Rioux, Albert Mitjans Coma, Romain Moyard, Lee J. OβRiordan, Mudit Pandey, Chae-Yeun Park, Borja Requena, Shuli Shu, Matthew Silverman, Jay Soni, Antal SzΓ‘va, Frederik Wilde, David Wierichs, Moritz Willmann.
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 π€