PennyLane v0.25 released

PennyLane team

The dreamiest version of PennyLane is now available for everyone to use. It comes with many new additions, including a brand new quantum resource estimation module, upgrades to operator arithmetic, differentiable error mitigation, more parameter broadcasting support, new measurement types, and more.

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

Estimate computational resource requirements 🧠

If you ever find yourself asking “How many qubits and gates do I need to run this algorithm?”, then you’ll love this brand new module!

The new resource module allows you to estimate the number of non-Clifford gates and logical qubits needed to implement quantum phase estimation algorithms for simulating materials and molecules. This includes support for quantum algorithms using first and second quantization with specific bases:

>>> n = 100000        # number of plane waves
>>> eta = 156         # number of electrons
>>> omega = 1145.166  # unit cell volume in atomic units
>>> algo = FirstQuantization(n, eta, omega)
>>> algo.gates
1.10e+13
>>> algo.qubits
4416
symbols  = ['O', 'H', 'H']
geometry = np.array([[0.00000000,  0.00000000,  0.28377432],
                    [0.00000000,  1.45278171, -1.00662237],
                    [0.00000000, -1.45278171, -1.00662237]], requires_grad = False)

mol = qml.qchem.Molecule(symbols, geometry, basis_name='sto-3g')
core, one, two = qml.qchem.electron_integrals(mol)()

algo = DoubleFactorization(one, two)
>>> print(algo.gates, algo.qubits)
103969925, 290

Intuitive operator arithmetic 🧮

It seems impossible to think that we can make adding, subtracting, and multiplying operators easier, but we did it 😤. So, what’s changed?

  • You can now sum and product arbitrary operators; previously, you could only take the sum and product of observables:
>>> qml.RX(0.2, wires=0) + qml.RX(0.1, wires=0)
RX(0.2, wires=[0]) + RX(0.1, wires=[0])
>>> qml.CRX(0.2, wires=[0, 1]) + qml.Projector([0, 1], wires=[0, 1])
CRX(0.2, wires=[0, 1]) + Projector([0, 1], wires=[0, 1])
  • You can now add scalars to operators:
>>> 4 + qml.PauliZ(0)
PauliZ(wires=[0]) + 4*(Identity(wires=[0]))
  • All existing operator functions you know and love, such as qml.eigvals and qml.matrix, continue to work with this upgraded operator arithemtic:
>>> op = 2 + qml.RX(0.2, wires=0) - 3.2 * qml.CRX(0.2, wires=[0, 1])
>>> qml.eigvals(op)
array([-0.53837236, -0.23245866, -0.17509958,  0.1579206 ])
  • New operator functions — such as qml.simplify — make working with operator arithmetic even easier:
>>> op = qml.adjoint(qml.adjoint(qml.RX(0.5, wires=0)))
>>> qml.simplify(op)
RX(0.5, wires=[0])

Each of these new functionalities can be used within QNodes as operators or observables, where applicable, while also maintaining differentiability:

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

@qml.qnode(dev)
def circuit(theta):
    qml.RX(theta, 0)**2
    return qml.expval(qml.PauliZ(0))
>>> theta = np.array([7.89], requires_grad=True)
>>> circuit(theta)
tensor(-0.99740648, requires_grad=True)
>>> qml.grad(circuit)(theta)
tensor([0.14394889], requires_grad=True)

Behind the scenes, this is enabled by some new operator functions, qml.op_sum for taking sums of operators, qml.prod for operator products, and qml.s_prod for scalar products of operators. For more details on using these low level arithmetic functions, check out the documentation.

Differentiable error mitigation ⚙

You know what they say: differentiable errors fix your circuit’s noisy terrors.

Elevate any variational quantum algorithm to a mitigated algorithm with improved results on noisy hardware while maintaining differentiability throughout.

In order to do so, use the qml.transforms.mitigate_with_zne transform on your QNode and provide the PennyLane proprietary qml.transforms.fold_global folding function and qml.transforms.poly_extrapolate extrapolation function. Here is an example for a noisy simulation device where we mitigate a QNode and are still able to compute the gradient:

# Describe noise
noise_gate = qml.DepolarizingChannel
noise_strength = 0.1

# Load devices
dev_ideal = qml.device("default.mixed", wires=n_wires)
dev_noisy = qml.transforms.insert(noise_gate, noise_strength)(dev_ideal)

scale_factors = [1, 2, 3]
@mitigate_with_zne(
  scale_factors,
  qml.transforms.fold_global,
  qml.transforms.poly_extrapolate,
  extrapolate_kwargs={'order': 2}
)
@qml.qnode(dev_noisy)
def qnode_mitigated(theta):
    qml.RY(theta, wires=0)
    return qml.expval(qml.PauliX(0))
>>> theta = np.array(0.5, requires_grad=True)
>>> qml.grad(qnode_mitigated)(theta)
0.5712737447327619

More native support for parameter broadcasting 📡

The introduction of parameter broadcasting last release was a huge hit 🏏. In this release, we have exciting additions to an already-useful feature.

default.qubit now natively supports parameter broadcasting, providing increased performance when executing the same circuit at various parameter positions compared to manually looping over parameters.

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

@qml.qnode(dev)
def circuit(x):
    qml.RX(x, wires=0)
    return qml.expval(qml.PauliZ(0))
>>> circuit(np.array([0.1, 0.3, 0.2]))
tensor([0.99500417, 0.95533649, 0.98006658], requires_grad=True) 

In addition, parameter-shift gradients now allow for parameter broadcasting internally, which can result in a significant speedup when computing gradients of circuits with many parameters. qml.gradients.param_shift now accepts the keyword argument broadcast. If set to True, broadcasting is used to compute the derivative:

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

@qml.qnode(dev)
def circuit(x, y):
    qml.RX(x, wires=0)
    qml.RY(y, wires=1)
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
>>> x = np.array([np.pi/3, np.pi/2], requires_grad=True)
>>> y = np.array([np.pi/6, np.pi/5], requires_grad=True)
>>> qml.gradients.param_shift(circuit, broadcast=True)(x, y)
(tensor([[-0.7795085,  0.       ],
          [ 0.       , -0.7795085]], requires_grad=True),
tensor([[-0.125, 0.  ],
        [0.  , -0.125]], requires_grad=True))

The following simpler example also makes use of broadcasting:

@qml.qnode(dev, diff_method="parameter-shift", broadcast=True)
def circuit(x, y):
    qml.RX(x, wires=0)
    qml.RY(y, wires=1)
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
>>> x = np.array(0.1, requires_grad=True)
>>> y = np.array(0.4, requires_grad=True)
>>> qml.grad(circuit)(x, y)
(array(-0.09195267), array(-0.38747287))

Here, only two circuits are created internally, compared to four with broadcast=False. Check out this speedup!

Finally, quantum chemistry operations and templates have also been updated to support parameter broadcasting.

>>> op = qml.SingleExcitation(np.array([0.3, 1.2, -0.7]), wires=[0, 1])
>>> op.matrix().shape
(3, 4, 4)

All-new measurements ✨

We’re upping our measurement game with new ergonomic features.

Counts

QNodes with shots != None that return qml.counts will yield a dictionary whose keys are bitstrings representing computational basis states that were measured, and whose values are the corresponding counts (i.e., how many times that computational basis state was measured):

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

@qml.qnode(dev)
def circuit():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.counts()
>>> circuit()
{'00': 495, '11': 505}

qml.counts can also accept observables, where the resulting dictionary is ordered by the eigenvalues of the observable.

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

@qml.qnode(dev)
def circuit():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.counts(qml.PauliZ(0)), qml.counts(qml.PauliZ(1))
>>> circuit()
({-1: 470, 1: 530}, {-1: 470, 1: 530})

New return types for QNodes with multiple measurements

Over the next couple of releases, we will be making a change to the behaviour of QNodes that return multiple measurements. This will ensure that PennyLane continues to seamlessly integrate with frameworks such as NumPy and SciPy, and unlock new and exciting features and improvements down the line.

Currently, QNodes that return multiple measurements either return a multidimensional array if the measurement dimension matches,

>>> @qml.qnode(dev)
... def circuit(x):
...     qml.RX(x, wires=0)
...     return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))
>>> circuit(0.5)
tensor([0.87758256, 1.        ], requires_grad=True)

or a flattened ragged array if the measurement dimensions do not match:

>>> @qml.qnode(dev)
... def circuit(x):
...     qml.RX(x, wires=0)
...     return qml.expval(qml.PauliZ(0)), qml.probs(wires=[0, 1])
>>> circuit(0.5)
tensor([0.87758256, 0.93879128, 0.        , 0.06120872, 0.        ], requires_grad=True)

To match consistency with other scientific Python packages, we will be moving to having multiple measurements be represented by a tuple of tensors, rather than a stacked or flattened array, over the next couple of releases.

This new behaviour is experimental and off by default, but can be tested today by using the new qml.enable_return() function:

>>> qml.enable_return()
>>> @qml.qnode(dev)
... def circuit(x):
...     qml.RX(x, wires=0)
...     return qml.expval(qml.PauliZ(0)), qml.probs(wires=[0, 1])
>>> circuit(0.5)
(tensor(0.87758256, requires_grad=True),
 tensor([0.93879128, 0.        , 0.06120872, 0.        ], requires_grad=True))

In addition, the boolean function qml.active_return() can be queried to determine if this new behaviour is activated, and disabled via qml.disable_return().

Over the upcoming releases, we will work to enable feature parity with this new return type behaviour, before making it the default.

Improvements 🛠

Get better everyday; that’s our motto.

  • When adjoint differentiation is requested, circuits are now decomposed so that all trainable operations have a generator.
  • The efficiency of the Hartree-Fock workflow has been improved by removing repetitive steps.
  • The qml.state and qml.density_matrix measurements now support custom wire labels.
  • lightning.gpu now has support for multi-GPU observable batching using the adjoint differentiation method.

  • lightning.qubit added new architecture specific kernel optimizations to ensure the best performance from the get-go.
  • Jacobians are now cached with the Autograd interface when using the parameter-shift rule.

Deprecations and breaking changes 💔

  • The deprecated qml.hf module is removed. Users with code that calls qml.hf can simply replace qml.hf with qml.qchem in most cases, or refer to the documentation and demos for more information.
  • Custom devices inheriting from DefaultQubit or QubitDevice can break due to the introduction of parameter broadcasting.

    A custom device should only break if all three following statements hold simultaneously:

    1. The custom device inherits from DefaultQubit, not QubitDevice.
    2. The device implements custom methods in the simulation pipeline that are incompatible with broadcasting (for example expval, apply_operation or analytic_probability).
    3. The custom device maintains the flag "supports_broadcasting": True in its capabilities dictionary or it overwrites Device.batch_transform without applying broadcast_expand (or both).

    The capabilities["supports_broadcasting"] is set to True for DefaultQubit. Typically, the easiest fix will be to change the capabilities["supports_broadcasting"] flag to False for the child device and/or to include a call to broadcast_expand in CustomDevice.batch_transform, similar to how Device.batch_transform calls it.

    Separately from the above, custom devices that inherit from QubitDevice and implement a custom _gather method need to allow for the kwarg axis to be passed to this _gather method.


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:

Ali Asadi, Juan Miguel Arrazola, Utkarsh Azad, Samuel Banning, Prajwal Borkar, Isaac De Vlugt, Olivia Di Matteo, Kristiyan Dilov, Amintor Dusko, David Ittah, Josh Izaac, Soran Jahangiri, Edward Jiang, Ankit Khandelwal, Korbinian Kottmann, Meenu Kumari, Christina Lee, Rashid N. H. M., Sergio Martínez-Losa, Albert Mitjans Coma, Ixchel Meza Chavez, Romain Moyard, Lee James O’Riordan, Mudit Pandey, Chae-Yeun Park, Bogdan Reznychenko, Shuli Shu, Jay Soni, Modjtaba Shokrian-Zini, Antal Száva, Trevor Vincent, David Wierichs, Moritz Willmann