PennyLane v0.17 released

PennyLane team

The latest release of PennyLane is now out and available for everyone to use. It comes with powerful new additions, such as support for circuit compilation routines, new gradient transforms, and a quantum device resource tracker. It also contains improvements and quality-of-life updates, including provided Docker images, new templates, and improved optimizers.

Quantum circuit optimization using compilation transforms

PennyLane can now perform quantum circuit optimization using the top-level transform qml.compile. The compile transform allows you to chain together sequences of specific transforms into custom circuit optimization pipelines.

The following optimization transforms are now available to use, either independently or within a compilation pipeline:

  • commute_controlled: push commuting single-qubit gates through controlled operations
  • cancel_inverses: remove adjacent pairs of operations that cancel out
  • merge_rotations: combine adjacent rotation gates of the same type into a single gate
  • single_qubit_fusion: convert each sequence of single-qubit operations into a single Rot gate

The default qml.compile behaviour shown below applies a single sequence of commute_controlled, cancel_inverses, and merge_rotations, though users can supply an ordered list of transforms to the pipeline keyword.

dev = qml.device('default.qubit', wires=[0, 1, 2])

@qml.qnode(dev)
@qml.compile()
def qfunc(x, y, z):
    qml.Hadamard(wires=0)
    qml.Hadamard(wires=1)
    qml.Hadamard(wires=2)
    qml.RZ(z, wires=2)
    qml.CNOT(wires=[2, 1])
    qml.RX(z, wires=0)
    qml.CNOT(wires=[1, 0])
    qml.RX(x, wires=0)
    qml.CNOT(wires=[1, 0])
    qml.RZ(-z, wires=2)
    qml.RX(y, wires=2)
    qml.PauliY(wires=2)
    qml.CZ(wires=[1, 2])
    return qml.expval(qml.PauliZ(wires=0))

Using qml.compile(), the above QNode is compiled into the following circuit:

>>> print(qml.draw(qfunc)(0.2, 0.3, 0.4))
0: ──H───RX(0.6)───────────────────┤ ⟨Z⟩
1: ──H──╭X─────────────────╭CY─────┤
2: ──H──╰C────────RX(0.3)──╰CY──Y──┤

You can read more about these transformations in the compilation documentation.

Faster and more intuitive VQE simulations

You can now leverage sparse Hamiltonians, and gain significant speed-ups in your simulations! Furthermore, the expectation values of Hamiltonians can be directly returned in your quantum circuits:

dev = qml.device("default.qubit", wires=2)
H = qml.Hamiltonian([1., 2., 3.],  [qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)])
w = qml.init.strong_ent_layers_uniform(1, 2, seed=1967)

@qml.qnode(dev)
def circuit(w):
    qml.templates.StronglyEntanglingLayers(w, wires=range(2))
    return qml.expval(H)
>>> print(circuit(w))
-1.5133943637878295
>>> print(qml.grad(circuit)(w))
[[[-8.32667268e-17  1.39122955e+00 -9.12462052e-02]
[ 1.02348685e-16 -7.77143238e-01 -1.74708049e-01]]]

QNodes are even more powerful

  • Return samples in the computational basis directly via qml.sample() in a QNode:

    dev = qml.device("default.qubit", wires=2, shots=5)
    
    @qml.qnode(dev)
    def circuit():
        qml.Hadamard(wires=0)
        qml.CNOT(wires=[0, 1])
        return qml.sample()
    
    >>> circuit()
    array([[0, 0],
           [1, 1],
           [0, 0],
           [0, 0],
           [1, 1]])
    
  • Operations that have been instantiated elsewhere can now be easily added to QNodes and other queuing contexts using qml.apply:

    op = qml.RX(0.4, wires=0)
    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RY(x, wires=0)
        qml.apply(op)
        return qml.expval(qml.PauliZ(0))
    

Device Resource Tracker

Use the new device tracker to track executions of a QNode, even when computing parameter-shift gradients. This functionality will improve the ease of monitoring large batches and remote jobs by providing easy access to the number of executions, batches, shots, and more.

dev = qml.device('default.qubit', wires=1, shots=100)

@qml.qnode(dev, diff_method="parameter-shift")
def circuit(x):
    qml.RX(x, wires=0)
    return qml.expval(qml.PauliZ(0))

x = np.array(0.1)

with qml.Tracker(circuit.device) as tracker:
    qml.grad(circuit)(x)
>>> tracker.totals
{'executions': 3, 'shots': 300, 'batches': 1, 'batch_len': 2}
>>> tracker.history
{'executions': [1, 1, 1],
'shots': [100, 100, 100],
'batches': [1],
'batch_len': [2]}
>>> tracker.latest
{'batches': 1, 'batch_len': 2}

Custom callback functions can also be provided; these callback functions are called whenever the tracker is updated. This makes it possible to monitor remote jobs or large parameter-shift batches.

>>> def shots_info(totals, history, latest):
...     print("Total shots: ", totals['shots'])
>>> with qml.Tracker(circuit.device, callback=shots_info) as tracker:
...     qml.grad(circuit)(0.1)
Total shots:  100
Total shots:  200
Total shots:  300
Total shots:  300

Docker containerization support

Getting started using or contributing to PennyLane is now even easier with provided Docker PennyLane images. There’s support for all interfaces (TensorFlow, Torch, and Jax), as well as plugins and QChem.

In addition, both CPU and GPU (Nvidia CUDA 11.1+) images are provided. See a more detailed description here.

Module for differentiable quantum gradient transforms

A new gradients module qml.gradients has been added, providing the new quantum gradient transforms:

  • qml.gradients.finite_diff
  • qml.gradients.param_shift
  • qml.gradients.param_shift_cv

These quantum gradient transforms act directly on low-level quantum tape datastructures, returning tapes to be executed on hardware, and a corresponding post-processing function.

>>> params = np.array([0.3,0.4,0.5], requires_grad=True)
>>> with qml.tape.JacobianTape() as tape:
...     qml.RX(params[0], wires=0)
...     qml.RY(params[1], wires=0)
...     qml.RX(params[2], wires=0)
...     qml.expval(qml.PauliZ(0))
...     qml.var(qml.PauliZ(0))
>>> tape.trainable_params = {0, 1, 2}
>>> gradient_tapes, fn = qml.gradients.param_shift(tape)

These gradient tapes can be executed on quantum devices and post-processed to compute the gradient:

>>> res = dev.batch_execute(gradient_tapes)
>>> fn(res)
array([[-0.69688381, -0.32648317, -0.68120105],
    [ 0.8788057 ,  0.41171179,  0.85902895]])

All gradient transforms are differentiable, unlocking higher-order derivatives on hardware.

New operations and templates

  • Play around with the new Grover Diffusion Operator template qml.templates.GroversOperator. For example, use it to perform Grover’s Search Algorithm with an oracle function that marks the “all ones” state with a negative sign.

    n_wires = 3
    wires = list(range(n_wires))
    
    def oracle():
        qml.Hadamard(wires[-1])
        qml.Toffoli(wires=wires)
        qml.Hadamard(wires[-1])
    
    dev = qml.device('default.qubit', wires=wires)
    
    @qml.qnode(dev)
    def GroverSearch(num_iterations=1):
        for wire in wires:
            qml.Hadamard(wire)
    
        for _ in range(num_iterations):
            oracle()
            qml.templates.GroverOperator(wires=wires)
    
        return qml.probs(wires)
    

    Running the above QNOde will yield the marked state with high probability:

    >>> GroverSearch(num_iterations=1)
    tensor([0.03125, 0.03125, 0.03125, 0.03125, 0.03125, 0.03125, 0.03125,
          0.78125], requires_grad=True)
    >>> GroverSearch(num_iterations=2)
    tensor([0.0078125, 0.0078125, 0.0078125, 0.0078125, 0.0078125, 0.0078125,
      0.0078125, 0.9453125], requires_grad=True)
    
  • Instances of QubitUnitary may now be decomposed directly to Rot operations, or RZ operations if the input matrix is diagonal. This can be achieved using the new decomposition method added to QubitUnitary or the quantum function transform unitary_to_rot().

    See the documentation for more details on this new quantum transform.

  • The qml.IsingYY has been added.

Improvements

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

  • The QNGOptimizer now accepts a custom grad_fn keyword argument to use for gradient computations, allowing it to be used with additional interfaces such as JAX.
  • The precision used by JAX simulations now matches the float precision indicated by the JAX configuration.
  • Quantum tape parameters can now be easily converted to NumPy arrays via the new qml.tape.Unwrap() context manager.

Breaking changes and deprecations

As new things are added, old things can be pruned away. Here’s what will be disappearing in the future:

  • The deprecated tape methods get_resources and get_depth have been removed, as they are superseded by the qml.specs function.
  • Specifying shots=None with qml.sample was previously deprecated. From this release onwards, setting shots=None when sampling will raise an error.
  • The existing pennylane.collections.apply function is no longer accessible via qml.apply, and needs to be imported directly from the collections package.

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:

Juan Miguel Arrazola, Olivia Di Matteo, Anthony Hayes, Theodor Isacsson, Josh Izaac, Soran Jahangiri, Nathan Killoran, Arshpreet Singh Khangura, Leonhard Kunczik, Christina Lee, Romain Moyard, Lee James O’Riordan, Ashish Panigrahi, Nahum Sá, Maria Schuld, Jay Soni, Antal Száva, David Wierichs.