PennyLane v0.21 released

PennyLane team

The latest release of PennyLane is now out and available for everyone to use. It comes with many new additions, including methods to reduce qubit counts, experimental Qiskit Runtime support, improved quantum aware optimizers, better JAX support, new transforms, templates, and more.

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

Reduce qubit counts with Hamiltonian tapering 🔽

As we explore larger and larger models, the quantum data we work with — such as molecular Hamiltonians — require more qubits to be accurately represented and embedded in our quantum circuits.

With this release, PennyLane introduces qubit tapering via qml.hf.transform_hamiltonian.

Take advantage of inherent symmetries of fermionic Hamiltonians to reduce the number of qubits required to represent the Hamiltonian — all while preserving the original structure of the Hamiltonian.

# molecular geometry
symbols = ["He", "H"]
geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4588684632]])
mol = qml.hf.Molecule(symbols, geometry, charge=1)

# generate the qubit Hamiltonian
H = qml.hf.generate_hamiltonian(mol)(geometry)

# determine Hamiltonian symmetries
generators, paulix_ops = qml.hf.generate_symmetries(H, len(H.wires))
opt_sector = qml.hf.optimal_sector(H, generators, mol.n_electrons)

# taper the Hamiltonian
H_tapered = qml.hf.transform_hamiltonian(H, generators, paulix_ops, opt_sector)

We can compare the number of qubits required by the original Hamiltonian and the tapered Hamiltonian:

>>> len(H.wires)
4
>>> len(H_tapered.wires)
2

For quantum chemistry algorithms, the Hartree-Fock state can also be tapered:

n_elec = mol.n_electrons
n_qubits = mol.n_orbitals * 2

hf_tapered = qml.hf.transform_hf(
    generators, paulix_ops, opt_sector, n_elec, n_qubits
)

Qiskit Runtime support 🏃‍♂️

With version 0.21, the PennyLane-Qiskit plugin now provides initial Qiskit Runtime support.

Previously, when using PennyLane with IBM quantum hardware via the PennyLane-Qiskit plugin, each circuit evaluation would be separately sent and queued for execution on hardware, which can slow down classical optimization loops with many iterations.

In the last release, we added support for batch execution of circuits, massively improving the time taken to submit multiple, independent circuit (for example, when computing quantum gradients).

With our experimental Qiskit Runtime support, more of the computation is moved server-side, further reducing overhead.

Qiskit Runtime support in PennyLane takes two forms: runtime devices and workflow runners.

Runtime devices

Two Qiskit Runtime devices are available for circuit sampling, circuit-runner and sampler. Simply instantiate the following devices, specify a backend, and provide any additional options:

dev = qml.device('qiskit.ibmq.circuit_runner', wires=2, backend='ibmq_qasm_simulator', shots=8000, **kwargs)
dev = qml.device('qiskit.ibmq.sampler', wires=2, backend='ibmq_qasm_simulator', shots=8000, **kwargs)

Workflow runners

Not all Qiskit Runtime programs correspond to complete devices, some solve specific problems such as VQE.

A custom Qiskit VQE program has been added and can be used via the vqe_runner function. Simply upload this Runtime program at the beginning of your session, retrieve the program ID, and use it within the VQE runner.

from pennylane_qiskit import upload_vqe_runner, vqe_runner
program_id = upload_vqe_runner(hub="ibm-q", group="open", project="main")

def vqe_circuit(params):
    qml.RX(params[0], wires=0)
    qml.RY(params[1], wires=0)

job = vqe_runner(
    program_id=program_id,
    backend="ibmq_qasm_simulator",
    hamiltonian=1.0 * qml.PauliX(0) + 1.0 * qml.PauliZ(0),
    ansatz=vqe_circuit,
    x0=[3.97507603, 3.00854038],
    shots=8000,
    optimizer="SPSA",
    optimizer_config={"maxiter": 40},
    kwargs={"hub": "ibm-q", "group": "open", "project": "main"},
)

For more details on Qiskit Runtime support, please see the PennyLane-Qiskit plugin . Note that Qiskit Runtime support is currently experimental — if you come across any issues, please let us know with a GitHub issue.

Improved quantum-aware optimizers 📉

Quantum-aware optimizers generally provide the best of both worlds here; taking into account the geometry of the quantum landscape to improve convergence while reducing quantum resources.

In this release we have significant performance and capability improvements across our suite of quantum-aware optimizers.

Rotosolve optimization with arbitrary circuits and processing

The Rotosolve optimizer — a quantum-aware optimization method which performs coordinate minimization within the quantum cost landscape — previously only supported variational circuits with a subset of supported gates, and no internal classical processing.

Now, circuits with arbitrary gates and linear classical processing are supported natively without decomposition, as long as the frequencies of the gate parameters are known. This new generalization extends the Rotosolve optimization method to a larger class of algorithms, and can reduce the cost of the optimization compared to decomposing all gates to single-qubit rotations.

Consider the following QNode, containing a mixture of single-qubit Pauli rotations, and more complicated parametrized gates such as a controlled Pauli-Y rotation.

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

@qml.qnode(dev)
def qnode(x, y):
    qml.RX(2.5 * x, wires=0)
    qml.CNOT(wires=[0, 1])
    qml.RZ(0.3 * y[0], wires=0)
    qml.CRY(1.1 * y[1], wires=[1, 0])
    return qml.expval(qml.PauliX(0) @ qml.PauliZ(1))

x = np.array(0.8, requires_grad=True)
y = np.array([-0.2, 1.5], requires_grad=True)

Its frequency spectra can be easily obtained via qml.fourier.qnode_spectrum:

>>> spectra = qml.fourier.qnode_spectrum(qnode)(x, y)
>>> spectra
{'x': {(): [-2.5, 0.0, 2.5]},
'y': {(0,): [-0.3, 0.0, 0.3], (1,): [-1.1, -0.55, 0.0, 0.55, 1.1]}}

We can then use this information to use Rotosolve to optimize this circuit:

>>> print("Initial cost:", np.round(qnode(x, y), 3))
Initial cost: 0.706
>>> opt = qml.RotosolveOptimizer()
>>> for _ in range(2):
...     x, y = opt.step(qnode, x, y, spectra=spectra)
...     print(f"New cost: {np.round(qnode(x, y), 3)}")
New cost: 0.0
New cost: -1.0

For more details, see the qml.RotosolveOptimizer documentation.

Speedier quantum natural gradients

A new function for computing the metric tensor on simulators, qml.adjoint_metric_tensor, has been added, that uses classically efficient methods to massively improve performance.

This new functionality can be used with qml.QNGOptimizer to speed up quantum natural gradient-based optimization.

Compare the performance against the hardware-compatible qml.metric_tensor on a 6 wire simulator:

>>> dev = qml.device("default.qubit", wires=4, shots=None)
>>> @qml.qnode(dev)
... def circuit(weights):
...     qml.StronglyEntanglingLayers(weights, wires=range(3))
...     return qml.expval(qml.PauliZ(0) @ qml.PauliZ(2))
>>> weights = np.random.random([4, 3, 3], requires_grad=True)
>>> %timeit qml.metric_tensor(circuit)(weights)
8.13 s ± 966 ms per loop
>>> %timeit qml.adjoint_metric_tensor(circuit)(weights)
5.05 s ± 327 ms per loop

For more details, see the qml.adjoint_metric_tensor documentation.

Better JAX support 🤖

If you’ve been using PennyLane + JAX + quantum hardware, the integration gets a whole lot better in this release with the support of vector-valued QNodes.

Previously, when using JAX with hardware or the parameter-shift rule, only QNodes that returned single expectation values were supported. In v0.21, you can now return multiple quantum measurement statistics, as well as vector-valued measurements such as qml.probs:

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

@qml.qnode(dev, diff_method="parameter-shift", interface="jax")
def circuit(x):
    qml.RX(x[0], wires=[0])
    qml.RY(x[1], wires=[1])
    qml.CNOT(wires=[0, 1])
    return qml.probs(wires=[1])
>>> x = jnp.array([0.543, -0.654])
>>> circuit(x, y)
DeviceArray([0.8397495 , 0.16025047], dtype=float32)
>>> jax.jacobian(circuit, argnums=[0, 1])(x, y)
DeviceArray([[-0.2050439,  0.26043  ],
            [ 0.2050439, -0.26043  ]], dtype=float32)

Note that jax.jit is not yet supported for vector-valued QNodes.

New templates and transforms 🏗️

Alongside the great new features above, we also have new (differentiable!) templates and transforms to share.

Two new tensor-network templates

Tensor-network templates create quantum circuit architectures where circuit blocks can be broadcast with the shape and connectivity of tensor networks. Two new tensor-network templates in this release includes:

  • qml.MPS for initializing matrix product state tensor networks
  • qml.TTN for creating tree tensor networks.

Hardware-compatible Hessian transform

A new quantum gradient transform, qml.gradients.param_shift_hessian, allows direct computation of second order derivatives on hardware, while minimizing the number of circuit executions.

>>> dev = qml.device("default.qubit", wires=2)
>>> @qml.qnode(dev)
... def circuit(x):
...     qml.RX(x[0], wires=0)
...     qml.RY(x[1], wires=0)
...     return qml.expval(qml.PauliZ(0))
>>> x = np.array([0.1, 0.2], requires_grad=True)
>>> qml.gradients.param_shift_hessian(circuit)(x)
tensor([[-0.97517033,  0.01983384],
        [ 0.01983384, -0.97517033]], requires_grad=True)

Improvements

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

  • Performance improvements for lightning.qubit: new highly-performant C++ kernels for quantum gates have been added to lightning.qubit. For more details, please see the lightning.qubit release notes. The new kernels significantly improve the runtime performance of PennyLane-Lightning for both differentiable and non-differentiable workflows:

  • PennyLane and lightning.qubit now support Python 3.10.
  • The qml.transforms.insert transform now supports inserting operations after or before specified gates in the circuit.
  • A more efficient method of Hamiltonian simplification has been added to the hf module as qml.hf.simplify.

    This function combines redundant terms in a Hamiltonian and eliminates terms with a coefficient smaller than a cutoff value, and will eventually replace the logic in Hamiltonian.simplify(). For example, the time to construct the Hamiltonian of LiH is reduced roughly by a factor of 20.

  • The QAOA module now accepts both NetworkX and RetworkX graphs as function inputs.
  • The CircuitGraph, used to represent circuits via directed acyclic graphs, now uses RetworkX for its internal representation. This results in significant speedup for algorithms that rely on a directed acyclic graph representation.
  • For subclasses of Operator where the number of parameters is known before instantiation, the num_params is reverted back to being a static property. This allows to programmatically know the number of parameters before an operator is instantiated without changing the user interface.

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

Breaking changes

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

Defining trainable parameters in Autograd

QNode arguments will no longer be considered trainable by default when using the Autograd interface.

In order to obtain derivatives with respect to a parameter, trainable parameters should be instantiated via PennyLane’s NumPy wrapper using the requires_grad=True attribute.

x = np.array([0.1, 0.2], requires_grad=True)
qml.grad(qnode)(x)

Alternatively, trainability can be indicated via the argnum keyword argument passed to qml.grad/qml.jacobian:

x = np.array([0.1, 0.2])
qml.grad(circuit, argnum=1)(0.5, x)

Jacobian output shapes when using Autograd

qml.jacobian now follows a different convention regarding its output shape.

Previously, qml.jacobian would attempt to stack the Jacobian for multiple QNode arguments, which succeeded whenever the arguments have the same shape:

>>> @qml.qnode(qml.device("default.qubit", wires=1))
... def circuit(x, y):
...     qml.RX(x, wires=0)
...     qml.RY(y, wires=0)
...     return qml.probs(wires=0)
>>> x = np.array(0.2, requires_grad=True)
>>> y = np.array(0.6, requires_grad=True)
>>> qml.jacobian(circuit)(x, y)
array([[-0.08198444, -0.27669361],
    [ 0.08198444,  0.27669361]])

With this release, for QNodes with multuple arguments, the output shape instead is a tuple, where each entry corresponds to one QNode argument and has the shape (*output_shape, *argument_shape).

>>> qml.jacobian(circuit)(x, y)
(array([-0.08198444,  0.08198444]), array([-0.27669361,  0.27669361]))

This ensures that the output shape of qml.jacobian is consistent, inline with the behaviour of other autodifferentiation frameworks, and does not depend on the QNode return value.

Note that the behaviour of qml.jacobian is unchanged if the QNode only accepts one trainable parameter, or if the argnum argument is provided.

Rotosolve arguments

The keyword arguments for the qml.RotosolveOptimizer have been modified; please refer to the latest docstring for more information.


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:

Guillermo Alonso-Linaje, Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Sam Banning, Thomas Bromley, Esther Cruz, Amintor Dusko, Christian Gogolin, Nathan Killoran, Christina Lee, Olivia Di Matteo, Diego Guala, Anthony Hayes, David Ittah, Josh Izaac, Soran Jahangiri, Edward Jiang, Ankit Khandelwal, Korbinian Kottmann, Romain Moyard, Lee James O’Riordan, Chae-Yeun Park, Tanner Rogalsky, Maria Schuld, Jay Soni, Antal Száva, David Wierichs, Shaoming Zhang.