PennyLane v0.28 released

Isaac De Vlugt

PennyLane v0.28 — the release so good, not even ChatGPT can fathom it 🤖! Check out all of the awesome new functionality below.

Custom measurement processes 📐

Spread your wings and get creative with the ability to create custom measurements using the new qml.measurements module!

Previously, PennyLane only came with built-in measurements that you can return from QNodes: qml.expval, qml.probs, etc., to name a few. Over time, we have added more, but now you can define your own measurement process function to use in QNodes.

Within qml.measurements are new subclasses that allow for the possibility to create measurements:

Creating a custom measurement involves making a class that inherits from one of the classes above. Here is an example that inherits from SampleMeasurement, where the new measurement, CountState, computes the number of obtained samples for a given state:

from pennylane.measurements import SampleMeasurement

class CountState(SampleMeasurement):
    def __init__(self, state: str):
        self.state = state  # string identifying the state, e.g. "0101"
        wires = list(range(len(state)))
        super().__init__(wires=wires)

    def process_samples(self, samples, wire_order, shot_range, bin_size):
        counts_mp = qml.counts(wires=self._wires)
        counts = counts_mp.process_samples(samples, wire_order, shot_range, bin_size)
        return counts.get(self.state, 0)

    def __copy__(self):
        return CountState(state=self.state)

The method process_samples must be defined in order to instruct how CountState processes the samples. Within it, a call to the familiar qml.counts measurement is made, its process_samples method is called to obtain measurement counts, and only counts for the specified state are returned.

We can now execute the new measurement in a QNode as follows.

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

@qml.qnode(dev)
def circuit(x):
    qml.RX(x, wires=0)
    return CountState(state="1")
>>> circuit(1.23)
tensor(3303., requires_grad=True)

Differentiability is also conserved for this new measurement process:

>>> x = qml.numpy.array(1.23, requires_grad=True)
>>> qml.grad(circuit)(x)
4715.000000000001
  • MeasurementTransform: represents a measurement process that requires the application of a batch transform

For more information about these new features, see the documentation for qml.measurements.

ZX-calculus 🧮

The magic of ZX-calculus is now at your fingertips! Envision quantum protocols like a boss. 😤

ZX-calculus is a graphical formalism designed to provide an intuitive way to look at quantum protocols as linear maps between qubits, which are called ZX-diagrams. A great example of how intuitive a ZX-diagram can be is the one representing a SWAP gate — consisting of three CNOT gates — as seen above.

With the help of the PyZX framework, QNodes decorated with @qml.transforms.to_zx will return a PyZX graph that represents the computation at hand in the language of ZX-calculus.

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

@qml.transforms.to_zx
@qml.qnode(device=dev)
def circuit(p):
    qml.RZ(p[0], wires=1),
    qml.RZ(p[1], wires=1),
    qml.RX(p[2], wires=0),
    qml.PauliZ(wires=0),
    qml.RZ(p[3], wires=1),
    qml.PauliX(wires=1),
    qml.CNOT(wires=[0, 1]),
    qml.CNOT(wires=[1, 0]),
    qml.SWAP(wires=[0, 1]),
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
>>> params = [5 / 4 * np.pi, 3 / 4 * np.pi, 0.1, 0.3]
>>> circuit(params)
Graph(20 vertices, 23 edges)

Information about PyZX graphs can be found in the PyZX Graphs API.

PubChem database access and new basis sets ⚛

PubChem database access

PennyLane, meet PubChem. PubChem, meet PennyLane. 🤝

The symbols and geometry of compounds from the PubChem database can now be accessed via qchem.mol_data().

>>> import pennylane as qml
>>> from pennylane.qchem import mol_data
>>> mol_data("BeH2")
(['Be', 'H', 'H'],
 tensor([[ 4.79404621,  0.29290755,  0.        ],
              [ 3.77945225, -0.29290755,  0.        ],
              [ 5.80882913, -0.29290755,  0.        ]], requires_grad=True))
>>> mol_data(223, "CID")
(['N', 'H', 'H', 'H', 'H'],
 tensor([[ 0.        ,  0.        ,  0.        ],
              [ 1.82264085,  0.52836742,  0.40402345],
              [ 0.01417295, -1.67429735, -0.98038991],
              [-0.98927163, -0.22714508,  1.65369933],
              [-0.84773114,  1.373075  , -1.07733286]], requires_grad=True))

New basis sets

On the basis of basis sets, PennyLane basically has it covered. 🙌

Enjoy the ability to use the 6-311g and CC-PVDZ basis sets in your qml.qchem code!

>>> symbols = ["H", "He"] 
>>> geometry = np.array([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0]], requires_grad=False)
>>> charge = 1
>>> basis_names = ["6-311G", "CC-PVDZ"] 
>>> for basis_name in basis_names:
...     mol = qml.qchem.Molecule(symbols, geometry, charge=charge, basis_name=basis_name)
...     print(qml.qchem.hf_energy(mol)())
[-2.84429531] 
[-2.84061284]

New transforms, operators, and more 😯

Enjoy this roux of the finest functionalities in town! 🧑‍🍳🍲

Purity

Calculating the purity of arbitrary quantum states is now supported. Purity can now be calculated in an analogous fashion to, say, the Von Neumann entropy.

>>> theta = np.array(np.pi/3, requires_grad=True)
>>> x = np.array([np.cos(theta), 0, 0, np.sin(theta)])
>>> qml.math.purity(x, [0, 1])
1.0
>>> qml.math.purity(np.outer(x, np.conj(x)), [0])
0.625
>>> dev = qml.device("default.mixed", wires=2)
>>> @qml.qnode(dev)
... def circuit(theta):
...    qml.QubitStateVector(np.array([np.cos(theta), 0, 0, np.sin(theta)]), wires=[0, 1])
...    return qml.state()
...
>>> qml.qinfo.transforms.purity(circuit, wires=[0])(theta)
0.625
>>> qml.qinfo.transforms.purity(circuit, wires=[0, 1])(theta)
1.0

As with the other methods in qml.qinfo, purity is fully differentiable:

>>> qml.grad(qml.qinfo.transforms.purity(circuit, wires=[0]))(theta)
(0.8660254037844384+0j)

Combine mid-circuit measurements

  • Multiple mid-circuit measurements can now be combined arithmetically to create new conditionals.
dev = qml.device("default.qubit", wires=3)

@qml.qnode(dev)
def circuit():
    qml.Hadamard(wires=0)
    qml.Hadamard(wires=1)
    m0 = qml.measure(wires=0)
    m1 = qml.measure(wires=1)
    combined = 2 * m1 + m0
    qml.cond(combined == 2, qml.RX)(1.3, wires=2)
    return qml.probs(wires=2)
>>> circuit()
[0.90843735 0.09156265]  

New operators

Parameteric operators

Three new parametric operators, qml.CPhaseShift00, qml.CPhaseShift01, and qml.CPhaseShift10, are now available. Each of these operators performs a phase shift akin to qml.ControlledPhaseShift but on different positions of the state vector.

>>> dev = qml.device("default.qubit", wires=2)
>>> @qml.qnode(dev)
>>> def circuit():
...     qml.PauliX(wires=1)
...     qml.CPhaseShift01(phi=1.23, wires=[0,1])
...     return qml.state()
...
>>> circuit()
tensor([0.        +0.j       , 0.33423773+0.9424888j, 
        1.        +0.j       , 0.        +0.j       ], requires_grad=True)

Create operators from a generator

Create operators defined from a generator via qml.ops.op_math.Evolution. qml.ops.op_math.Evolution defines the exponential of an operator \(\hat{O}\), of the form \(e^{ix\hat{O}}\), with a single trainable parameter, \(x\). Limiting to a single trainable parameter allows the use of qml.gradients.param_shift to find the gradient with respect to the parameter \(x\).

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

@qml.qnode(dev, diff_method=qml.gradients.param_shift)
def circuit(phi):
    qml.ops.op_math.Evolution(qml.PauliX(0), -.5 * phi)
    return qml.expval(qml.PauliZ(0))
>>> phi = np.array(1.2)
>>> circuit(phi)
tensor(0.36235775, requires_grad=True)
>>> qml.grad(circuit)(phi)
-0.9320390495504149

Qutrit Hadamard

The qutrit Hadamard gate, qml.THadamard, is now available. This operation accepts a subspace keyword argument, which determines which variant of the qutrit Hadamard to use.

>>> th = qml.THadamard(wires=0, subspace=[0, 1])
>>> qml.matrix(th)
array([[ 0.70710678+0.j,  0.70710678+0.j,  0.        +0.j],
      [ 0.70710678+0.j, -0.70710678+0.j,  0.        +0.j],
      [ 0.        +0.j,  0.        +0.j,  1.        +0.j]])

Improvements 🛠

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

  • PennyLane now supports Python 3.11 and Torch v1.13. Support for Python 3.7 has been dropped.
  • When qml.probs, qml.counts, and qml.sample are called with no arguments, they measure all wires. Calling any of the aforementioned measurements with an empty wire list (e.g., qml.sample(wires=[])) will raise an error.

Deprecations and breaking changes 💔

As new things are added, outdated features are removed. To keep track of things in the deprecation pipeline, we’ve created a deprecation page.

Here’s what has changed in this release:

  • Python 3.7 support is no longer maintained. PennyLane will be maintained for versions 3.8 and up.

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, Juan Miguel Arrazola, Utkarsh Azad, Samuel Banning, Thomas Bromley, Astral Cai, Albert Mitjans Coma, Ahmed Darwish, Isaac De Vlugt, Olivia Di Matteo, Amintor Dusko, Pieter Eendebak, Lillian M. A. Frederiksen, Diego Guala, Katharine Hyatt, Josh Izaac, Soran Jahangiri, Edward Jiang, Korbinian Kottmann, Christina Lee, Romain Moyard, Lee James O’Riordan, Mudit Pandey, Kevin Shen, Matthew Silverman, Jay Soni, Antal Száva, David Wierichs, Moritz Willmann, and Filippo Vicentini.

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.