PennyLane v0.22 released

PennyLane team

The latest release of PennyLane is now out and available for everyone to use. It comes with many new additions, including executing large circuits with fewer qubits, differentiable mid-circuit measurements, a new high-performance GPU simulator, tools for quantum debugging, better batching support, and more.

Check out the table of contents below, or keep reading to find out more.

Cut your circuits into fragments for execution with fewer qubits βœ‚

When building new quantum algorithms, we often want to push the limits of what is possible, and test or deploy our algorithms on more and more wires. Unfortunately, all too often we end up constrained in our quest for more qubits by the underlying hardware or simulator device we are using.

With PennyLane v0.22, you can now execute a quantum algorithm that requires N wires on fewer than N wires, by taking advantage of circuit cutting.

Simply ‘cut’ wires within your QNode, and PennyLane will partition your algorithm into smaller fragments for execution, before combining and post-processing the results.

Circuit cutting is enabled by decorating a QNode with the @qml.cut_circuit transform. For example, to execute a three-wire circuit on a two-wire device:

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

@qml.cut_circuit
@qml.qnode(dev)
def circuit(x):
    qml.RX(x, wires=0)
    qml.RY(0.9, wires=1)
    qml.RX(0.3, wires=2)
    qml.CZ(wires=[0, 1])
    qml.RY(-0.4, wires=0)
    # cut the circuit into two fragments at wire 1
    qml.WireCut(wires=1)
    qml.CZ(wires=[1, 2])
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliZ(2))

Instead of executing the circuit directly, it will be partitioned into smaller fragments according to the qml.WireCut locations, and each fragment executed multiple times. Combining the results of the fragment executions will recover the expected output of the original uncut circuit:

>>> x = np.array(0.531, requires_grad=True)
>>> circuit(0.531)
0.47165198882111165

As always, circuit cutting support is also differentiable:

>>> qml.grad(circuit)(x)
-0.276982865449393

Note that while circuit cutting allows for executing a circuit on a device with fewer qubits, cutting a circuit can be expensive in terms of the number of executions required as well as additional classical postprocessing. For more details on circuit cutting, check out the qml.cut_circuit documentation page.

Quantum teleport with mid-circuit measurements πŸŒ€

One of the most highly requested features over the years has been the ability to perform quantum teleportation with PennyLane, and this is now unlocked πŸ”“ in version 0.22, with the introduction of mid-circuit measurements and conditional operations.

Conditional operations

  • qml.measure() allows circuit measurements to be placed in the middle of a quantum function.
  • The new qml.cond() transform allows operations to be conditioned on the result of a previous measurement.

Use mid-circuit measurements and conditional operations to build and run algorithms such as quantum teleportation, quantum error correction, and quantum error mitigation.

For example, the code below shows how to teleport a qubit from wire 0 to wire 2:

dev = qml.device("default.qubit", wires=3)
input_state = np.array([1, -1], requires_grad=False) / np.sqrt(2)

@qml.qnode(dev)
def teleport(state):
    # Prepare input state
    qml.QubitStateVector(state, wires=0)

    # Prepare Bell state
    qml.Hadamard(wires=1)
    qml.CNOT(wires=[1, 2])

    # Apply gates
    qml.CNOT(wires=[0, 1])
    qml.Hadamard(wires=0)

    # Measure first two wires
    m1 = qml.measure(0)
    m2 = qml.measure(1)

    # Condition final wire on results
    qml.cond(m2 == 1, qml.PauliX)(wires=2)
    qml.cond(m1 == 1, qml.PauliZ)(wires=2)

    # Return state on final wire
    return qml.density_matrix(wires=2)
>>> output_state = teleport(input_state)
>>> output_state
tensor([[ 0.5+0.j, -0.5+0.j],
        [-0.5+0.j,  0.5+0.j]], requires_grad=True)

We can double-check that the qubit has been teleported by computing the overlap between the input state and the resulting state on wire 2:

>>> input_state.conj() @ output_state @ input_state
tensor(1.+0.j, requires_grad=True)

Train mid-circuit measurements by deferring them

If a device doesn’t natively support mid-circuit measurements, the @qml.defer_measurements transform can be applied to the QNode to transform the QNode into one with terminal measurements and controlled operations:

@qml.qnode(dev)
@qml.defer_measurements
def circuit(x):
    qml.Hadamard(wires=0)
    m = qml.measure(0)
    qml.cond(
        m == 1,                         # measurement condition
        lambda: qml.RX(x**2, wires=1),  # qfunc to apply if condition is True (m==1)
        lambda: qml.RY(x, wires=1)      # qfunc to apply if condition is False (m!=1)
    )()
    return qml.expval(qml.PauliZ(1))
>>> x = np.array(0.7, requires_grad=True)
>>> print(qml.draw(circuit, expansion_strategy="device")(x))
0: ──H─╭C─────────X─╭C─────────X──
1: ────╰RX(0.49)────╰RY(0.70)─────  <Z>
>>> circuit(x)
tensor(0.82358752, requires_grad=True)

Deferring mid-circuit measurements also enables differentiation: don’t just evaluate your mid-circuit algorithms, but train πŸš‚ them as well!

>>> qml.grad(circuit)(x)
-0.651546965338656

For a full description of new capabilities, refer to the Mid-circuit measurements and conditional operations section in the documentation.

Accelerate your simulations with cuQuantum GPU support βš‘

We are excited to announce the release of GPU support for our high-performance lightning.qubit simulator: lightning.gpu. This new device utilizes the NVIDIA cuQuantum library under-the-hood, and includes efficient computation of quantum gradients via adjoint differentiation.

Use lightning.gpu for a significant speed-up for large quantum circuit evaluations, even when using multiple CPU-threads:

The lightning.gpu device can be installed via pip:

pip install pennylane-lightning[gpu]

Once installed, it can be loaded and used with any PennyLane QNodes:

dev = qml.device("lightning.gpu", wires=22)

@qml.qnode(dev, diff_method="adjoint")
def circuit(weights):
    qml.StronglyEntanglingLayers(weights, wires=list(range(n_wires)))
    return [qml.expval(qml.PauliZ(i)) for i in range(n_wires)]

param_shape = qml.StronglyEntanglingLayers.shape(n_layers=2, n_wires=22)
params = np.random.random(param_shape)
jac = qml.jacobian(circuit)(params)

For more details on installing and using lightning.gpu, check out the device documentation.

Debug with mid-circuit quantum snapshots πŸ“·

Designing new quantum algorithms is tough, and debugging quantum algorithms is sometimes even harder!

To help alleviate the frustration of (quantum) debugging, v0.22 of PennyLane introduces the qml.Snapshot operation, which saves the internal state of simulator devices at arbitrary points within the quantum execution.

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

@qml.qnode(dev, interface=None)
def circuit():
    qml.Snapshot()
    qml.Hadamard(wires=0)
    qml.Snapshot("very_important_state")
    qml.CNOT(wires=[0, 1])
    qml.Snapshot()
    return qml.expval(qml.PauliX(0))

During normal execution, the snapshots are ignored:

>>> circuit()
array(0.)

However, when using the qml.snapshots transform, intermediate values of the statevector will be stored and returned alongside the results:

>>> qml.snapshots(circuit)()
{0: array([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]),
'very_important_state': array([0.70710678+0.j, 0.        +0.j, 0.70710678+0.j, 0.        +0.j]),
2: array([0.70710678+0.j, 0.        +0.j, 0.        +0.j, 0.70710678+0.j]),
'execution_results': array(0.)}

Currently, snapshots are supported on default.qubit, default.mixed, and default.gaussian, returning representations of the quantum state. Over time, we plan to add more options for snapshots, across both simulator and hardware devices.

Better batching πŸ“¦

We previously added the @qml.batch_params transform to enable batching over trainable parameters in your quantum algorithms. In this release, we extend this support to all gate parameters, including embeddings and state preparation, via @qml.batch_input.

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

@qml.batch_input(argnum=0)
@qml.qnode(dev, diff_method="parameter-shift", interface="tf")
def circuit(inputs, weights):
    # add a batch dimension to the embedding data
    qml.AngleEmbedding(inputs, wires=range(2), rotation="Y")
    qml.RY(weights[0], wires=0)
    qml.RY(weights[1], wires=1)
    return qml.expval(qml.PauliZ(1))

Batched input parameters can then be passed during QNode evaluation:

>>> import tensorflow as tf
>>> x = tf.random.uniform((10, 2), 0, 1)
>>> w = tf.random.uniform((2,), 0, 1)
>>> circuit(x, w)
<tf.Tensor: shape=(10,), dtype=float64, numpy=
array([0.46230079, 0.73971315, 0.95666004, 0.5355225 , 0.66180948,
        0.44519553, 0.93874261, 0.9483197 , 0.78737918, 0.90866411])>

Even more mighty quantum transforms πŸ›βž‘πŸ¦‹

From circuit cutting, to conditional operations, snapshots, deferred measurements, and input batching, you must be thinking ‘Wow! This release couldn’t possibly fit even more transforms!’

And yet! We have barely scratched the surface. Here is a quick rundown of more quantum transforms that are debuting in version 0.22:

  • qml.matrix() for computing the matrix representation of one or more unitary operators.
  • qml.eigvals() for computing the eigenvalues of one or more operators.
  • qml.generator() for computing the generator of a single-parameter unitary operation.
  • qml.commutation_dag() to construct the pairwise-commutation directed acyclic graph (DAG) representation of a quantum circuit.

As always, all transforms support a functional user interface with differentiation support (where possible):

>>> def circuit(theta):
...     qml.RX(theta, wires=1)
...     qml.PauliZ(wires=0)
>>> qml.matrix(circuit)(np.pi / 4)
array([[ 0.92387953+0.j,  0.+0.j ,  0.-0.38268343j,  0.+0.j],
[ 0.+0.j,  -0.92387953+0.j,  0.+0.j,  0. +0.38268343j],
[ 0. -0.38268343j,  0.+0.j,  0.92387953+0.j,  0.+0.j],
[ 0.+0.j,  0.+0.38268343j,  0.+0.j,  -0.92387953+0.j]])

Improvements

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

  • Most compilation transforms, and relevant subroutines, have been updated to support just-in-time compilation with @jax.jit.
  • The frequencies of gate parameters are now accessible as an operation property, and are used for circuit analysis, optimization via the RotosolveOptimizer, and differentiation with the parameter-shift rule (including the general shift rule).
  • When computing gradients on quantum hardware, the parameter-shift rule now uses parameter frequencies to compute general shift rules when operation gradient recipes are not defined. This enables hardware gradient support on a wider set of operations.
  • The text-based drawer accessed via qml.draw() has been improved, with new options for displaying parameters and matrices, an improved algorithm for determining gate positions, and cosmetic improvements.

For the full list of improvements, please refer to the release notes.

Deprecations and breaking changes

As new things are added, outdated features are removed. Here’s what will be changing in this release.

Deprecations

  • qml.transforms.get_unitary_matrix() has been deprecated and will be removed in a future release. For extracting matrices of operations and quantum functions, please use qml.matrix().
  • The qml.finite_diff() function has been deprecated and will be removed in an upcoming release. Instead, qml.gradients.finite_diff() can be used to compute purely quantum gradients (that is, gradients of tapes or QNodes).
  • The MultiControlledX operation now accepts a single wires keyword argument for both control_wires and wires. The single wires keyword should contain all control wires followed by a single target wire.
  • Executing tapes using tape.execute(dev) is deprecated. Please use the qml.execute([tape], dev) function instead.
  • The subclasses of the quantum tape, including JacobianTape, are deprecated. Instead of calling JacobianTape.jacobian() please use a standard QuantumTape, and apply gradient transforms using the qml.gradients module.

In addition, there are several important changes when creating custom operations:

  • The Operator.matrix method has been deprecated and Operator.compute_matrix should be defined instead. Operator matrices can be accessed using qml.matrix(op).
  • The Operator.decomposition method has been deprecated and Operator.compute_decomposition should be defined instead. Operator decompositions can be accessed using Operator.decomposition().
  • The Operator.eigvals method has been deprecated and Operator.compute_eigvals should be defined instead. Operator eigenvalues can be accessed using qml.eigvals(op).
  • The Operator.generator property is now a method, and should return an operator instance representing the generator. Operator generators can be accessed using qml.generator(op).

For more details on adding custom operations in version 0.22, please see the Adding new operators page in the documentation.

Breaking changes

  • The pennylane.measure module has been renamed to pennylane.measurements.

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:

Catalina Albornoz, Jack Y. Araz, Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Sam Banning, Thomas Bromley, Olivia Di Matteo, Christian Gogolin, Diego Guala, Anthony Hayes, David Ittah, Josh Izaac, Soran Jahangiri, Nathan Killoran, Christina Lee, Angus Lowe, Maria Fernanda Morris, Romain Moyard, Zeyue Niu, Lee James O’Riordan, Chae-Yeun Park, Maria Schuld, Jay Soni, Antal SzΓ‘va, Trevor Vincent, and David Wierichs.