PennyLane v0.29 released

Isaac De Vlugt, Thomas Bromley

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 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)

@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])


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

a = jnp.array(0.1)
b = jnp.array(0.2)

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

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 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)]
>>>, ops)
(1.1*(PauliX(wires=[0]))) + (2.2*(PauliY(wires=[0])))
>>>, 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 and qml.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 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 and gradient_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 from default.qubit to default.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 the Operation.inverse setter have been removed. Please use qml.adjoint or qml.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, 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.

Author Biography

Isaac De Vlugt

Isaac De Vlugt

Isaac De Vlugt is a quantum computing educator at Xanadu. His work involves creating accessible quantum computing content for the community, as well as spamming GIFs in our Slack channels.

Thomas Bromley

Thomas Bromley

Thomas is a quantum scientist working at Xanadu. His work is focused on developing software to execute quantum algorithms on simulators and hardware.