Ready for a new decade of adventures — bring on the 30s 🕺💃! PennyLane v0.30 is out! Check out all of the awesome new functionality below.

Pulse programming on hardware ⚛️🔬

In v0.29, we introduced pulse-based circuit simulation. In v0.30, we have extended this to the real deal: hardware. You can now run pulse programs on the Aquila device from QuEra, giving you access to 256 rubidium-atom qubits through the Amazon Braket service! 🎉

Shout out to the Amazon Braket and QuEra teams for this collab. 🤝

pulse prog hardware

We're so excited about pulse programming on hardware that we'll be releasing more content over the rest of this month. Stay tuned for a demo that will walk you through the details of hardware device access.

Loading a hardware-compatible Hamiltonian

PennyLane now provides time-dependent Hamiltonians of a specific form that are compatible with quantum hardware devices. The Aquila device consists of Rydberg atoms — the foundational unit for neutral atom quantum computing. In PennyLane, a Rydberg-system Hamiltonian can be constructed from a drive termqml.pulse.rydberg_drive — and an interaction termqml.pulse.rydberg_interaction:

import pennylane as qml
from jax import numpy as jnp

atom_coordinates = [[0, 0], [0, 4], [4, 0], [4, 4]]
wires = [0, 1, 2, 3]

amplitude = lambda p, t: p * jnp.sin(jnp.pi * t) ** 2
phase = 0
detuning = 3 * jnp.pi / 4

H_d = qml.pulse.rydberg_drive(amplitude, phase, detuning, wires)
H_i = qml.pulse.rydberg_interaction(atom_coordinates, wires)
H = H_d + H_i

The Aquila device

The QuEra Aquila device can be loaded in PennyLane using the PennyLane-Braket Plugin:

s3 = ("my-bucket", "my-prefix")  # update to your choice of bucket
arn = "arn:aws:braket:us-east-1::device/qpu/quera/Aquila"  # where Aquila lives
dev = qml.device("braket.aws.ahs", device_arn=arn, s3_destination_folder=s3, wires=4)

# local device to use if you do not yet have access to Braket
# or if Aquila is offline:
# dev = qml.device("braket.local.ahs", wires=4)

You will need to take a few steps to get the above to work:

To see when the Aquila device is accessible, visit the device's page on the Amazon Braket website.

Note that running the following circuit will result in a charge to your AWS account. Check out the pricing details for more information.

Executing on quantum hardware

It's now time to create a complete PennyLane circuit, combining the target time-dependent Hamiltonian with the hardware it will be run on:

def circuit(params):
    qml.evolve(H)(params, t=[0, 4])
    return qml.expval(qml.PauliZ(0))

All that is left to do is to execute it in the usual PennyLane way and your circuit will be whisked off behind the scenes to be run on Aquila.

>>> params = jnp.array([2.4])
>>> circuit(params)
Array(0.94307977, dtype=float32)

What's next?

As well as excitement, we also want to offer a note of caution: this is a brand new device and pulse-level programming is a new paradigm in PennyLane — there are bound to be a few rough edges here and there. If you notice any issues or bugs, please reach out to us on the PennyLane discussion forum or open an issue on GitHub 🐛.

After this, we'll be releasing more pulse-programming content in the coming weeks as well as improving support for this functionality in future releases.

Quantum singular value transformation 📦🏭


Providing access to state-of-the-art quantum algorithms is part of PennyLane's DNA, and in this release we've gone with one of the big ones. The quantum singular value transformation (QSVT) is a powerful quantum algorithm that can be seen as a generalization of established protocols like amplitude amplification. It's now available for you to play with in PennyLane!

My first QSVT

At its core, QSVT describes how a quantum circuit can be constructed to apply a polynomial transformation to the singular values of an input matrix. Consider a matrix A along with a vector angles that describes the target polynomial transformation. The qml.qsvt function creates a corresponding circuit:

import pennylane as qml
from pennylane import numpy as np

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

A = np.array([[0.1, 0.2], [0.3, 0.4]])
angles = np.array([0.1, 0.2, 0.3])

def example_circuit(A):
    qml.qsvt(A, angles, wires=[0, 1])
    return qml.expval(qml.PauliZ(wires=0))

This circuit is composed of qml.BlockEncode and qml.PCPhase operations.

>>> example_circuit(A)
tensor(0.97777078, requires_grad=True)
>>> print(example_circuit.qtape.expand(depth=1).draw(decimals=2)) 
0: ─╭∏_ϕ(0.30)─╭BlockEncode(M0)─╭∏_ϕ(0.20)─╭BlockEncode(M0)†─╭∏_ϕ(0.10)─┤  <Z>
1: ─╰∏_ϕ(0.30)─╰BlockEncode(M0)─╰∏_ϕ(0.20)─╰BlockEncode(M0)†─╰∏_ϕ(0.10)─┤

A more flexible QSVT

The qml.qsvt function creates a circuit that is targeted at simulators due to the use of matrix-based operations. For advanced users, you can use the qml.QSVT template to perform the transformation with a custom choice of unitary and projector operations, which may be hardware-compatible if a decomposition is provided.

For example, matrix- and phase-encoding operations can be defined manually and fed into qml.QSVT:

block_encode = qml.BlockEncode(A, wires=[0, 1])
shifts = [qml.PCPhase(i, dim=2, wires=[0, 1]) for i in reversed(angles)]

def example_circuit():
    qml.QSVT(block_encode, shifts)
    return qml.expval(qml.PauliZ(wires=0))

Evaluating this circuit gives the same result as above, but we have the option to swap out block_encode and shifts with our own choice of operations. If these operations have a defined decomposition into elementary gates, then the QSVT algorithm can be hardware-compatible.

Intuitive QNode returns ↩️

new return types

QNodes now return exactly what you tell them to! 🎉

This was an experimental feature introduced in v0.25 of PennyLane that was enabled via qml.enable_return(). Now, it's the default return system. Here's how it works.

Consider the following circuit:

import pennylane as qml

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

def circuit(x):
    qml.RX(x, wires=0)
    return qml.expval(qml.PauliZ(0)), qml.probs(0)

In v0.29 and earlier, circuit() would return a single length-3 array:

>>> circuit(0.5)
tensor([0.87758256, 0.93879128, 0.06120872], requires_grad=True)

In v0.30 and above, circuit() returns a length-2 tuple containing the expectation value and probabilities separately:

>>> circuit(0.5)
(tensor(0.87758256, requires_grad=True),
  tensor([0.93879128, 0.06120872], requires_grad=True))

More details about this change, along with help and troubleshooting tips to solve any issues, can be found on our dedicated QNode returns page. If you can't find the information you need here, you're confused, or have general questions about the new return system, post on the PennyLane discussion forum and let us know.

New devices and capabilities 🤩

As always, more devices are being added to the PennyLane ecosystem. At the same time, we're squeezing more and more capabilities out of existing devices. Here are some of the highlights from the past few months.

Access to Quantum Inspire

quantum inspire

The Quantum Inspire platform provides access to a range of simulator and hardware backends. Run by QuTech, it allows you to interact with quantum-dot based hardware as well as transmon qubits. Now, you can do all of this from the comfort of PennyLane 😌👌😎. Check out the PennyLane-QuantumInspire plugin to get started — and thanks to the Quantum Inspire team for creating this plugin 🙏!

PennyLane goes Kokkos

pennylane lightning kokkos performance

We're excited to announce the lightning.kokkos device, allowing you to run lightning-fast quantum simulations everywhere, from CPU to GPU ⚡🧑‍💻! If you haven't seen it already, check out our recent blog post for more details.

MCMC sampling in Lightning

PennyLane's lightning.qubit device now offers a Markov chain Monte Carlo (MCMC) backend for finite-shot calculations. Using MCMC sampling saves you from having to compute the full $2^N$-dimensional probability distribution of your $N$-qubit circuit, unlocking potential performance improvements. You can explore MCMC sampling by enabling it when instantiating your device:

dev = qml.device("lightning.qubit", wires=2, shots=1000, mcmc=True)

Take a look here for more details!

Improvements 🛠

In addition to the new features listed above, the release contains a wide array of improvements and optimizations:

  • Single-qubit operations that have multi-qubit control can now be decomposed more efficiently using fewer CNOT gates. Three decompositions from arXiv:2302.06377 are provided and compare favourably to the already-available qml.ops.ctrl_decomp_zyz:
wires = [0, 1, 2, 3, 4, 5]
control_wires = wires[1:]

@qml.qnode(qml.device('default.qubit', wires=6))
def circuit():
    with qml.QueuingManager.stop_recording():
        # the decomposition does not un-queue the target
        target = qml.RX(np.pi/2, wires=0)
    qml.ops.ctrl_decomp_bisect(target, (1, 2, 3, 4, 5))
    return qml.state()
>>> print(qml.draw(circuit, expansion_strategy="device")())
0: ──H─╭X──U(M0)─╭X──U(M0)†─╭X──U(M0)─╭X──U(M0)†──H─┤  State
1: ────├●────────│──────────├●────────│─────────────┤  State
2: ────├●────────│──────────├●────────│─────────────┤  State
3: ────╰●────────│──────────╰●────────│─────────────┤  State
4: ──────────────├●───────────────────├●────────────┤  State
5: ──────────────╰●───────────────────╰●────────────┤  State
  • The adjoint differentiation method can now be more efficient, avoiding the decomposition of operations that can be differentiated directly. Any operation that defines a generator() can be differentiated with the adjoint method.

  • Derivatives are computed more efficiently when using jax.jit with gradient transforms; the trainable parameters are now set correctly instead of every parameter having to be set as trainable. In the circuit below, only the derivative with respect to parameter b is now calculated:

import jax

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

@qml.qnode(dev, interface="jax-jit")
def circuit(a, b):
    qml.RX(a, wires=0)
    qml.RY(b, wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0))

a = jnp.array(0.4)
b = jnp.array(0.5)

jac = jax.jacobian(circuit, argnums=[1])
jac_jit = jax.jit(jac)

jac_jit(a, b)
assert len(circuit.tape.trainable_params) == 1
  • qml.QubitStateVector.state_vector now supports broadcasting.

  • qml.SparseHamiltonian can now be applied to any wires in a circuit rather than being restricted to all wires in the circuit.

  • Operators can now be divided by scalars with / with the addition of the Operation.__truediv__ dunder method.

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:

  • Both JIT interfaces are no longer compatible with JAX >0.4.3. We raise an error for those versions.

  • Evolution now initializes the coefficient with a factor of -1j instead of 1j.

  • The keyword argument argnums is now used for gradient transforms using JAX instead of argnum. argnum is automatically converted to argnums when using JAX and will no longer be supported in v0.31 of PennyLane.

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:

Komi Amiko, Ali Asadi, Utkarsh Azad, Thomas Bromley, Isaac De Vlugt, Olivia Di Matteo, Amintor Dusko, Lillian M. A. Frederiksen, Diego Guala, Soran Jahangiri, Korbinian Kottmann, Christina Lee, Vincent Michaud-Rioux, Albert Mitjans Coma, Romain Moyard, Lee J. O'Riordan, Mudit Pandey, Shuli Shu, Matthew Silverman, Jay Soni, David Wierichs, Trevor Vincent.

About the authors

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.